vibespot 0.9.2 → 0.9.4
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/assets/extraction-prompt.md +49 -0
- package/dist/index.js +152 -122
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
- package/ui/code-editor.js +393 -0
- package/ui/dashboard.js +152 -0
- package/ui/dialog.js +155 -0
- package/ui/index.html +38 -1
- package/ui/styles.css +286 -4
- package/ui/vendor/codemirror-bundle.global.js +24 -0
- package/ui/vendor/marked.umd.js +74 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibespot",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "AI-powered HubSpot CMS landing page builder — vibe coding & React converter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,11 +21,18 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
23
23
|
"@clack/prompts": "^0.9.1",
|
|
24
|
+
"@codemirror/lang-css": "^6.3.1",
|
|
25
|
+
"@codemirror/lang-html": "^6.4.11",
|
|
26
|
+
"@codemirror/lang-javascript": "^6.2.5",
|
|
27
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
28
|
+
"@codemirror/theme-one-dark": "^6.1.3",
|
|
24
29
|
"busboy": "^1.6.0",
|
|
25
30
|
"chalk": "^5.4.1",
|
|
31
|
+
"codemirror": "^6.0.2",
|
|
26
32
|
"commander": "^13.1.0",
|
|
27
33
|
"execa": "^9.5.2",
|
|
28
34
|
"mammoth": "^1.11.0",
|
|
35
|
+
"marked": "^17.0.4",
|
|
29
36
|
"pdf-parse": "^2.4.5",
|
|
30
37
|
"ws": "^8.19.0"
|
|
31
38
|
},
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code editor view — file browser + CodeMirror 6 editor.
|
|
3
|
+
* Toggles with the preview in the right panel.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let codeEditorView = null;
|
|
7
|
+
let codeFiles = [];
|
|
8
|
+
let currentCodeFile = null;
|
|
9
|
+
let codeDirty = false;
|
|
10
|
+
let codeSavedSinceSwitch = false;
|
|
11
|
+
|
|
12
|
+
function prettyJson(str) {
|
|
13
|
+
try { return JSON.stringify(JSON.parse(str), null, 2); } catch { return str || ""; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// File tree building
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
function buildCodeFileTree(data) {
|
|
21
|
+
const files = [];
|
|
22
|
+
|
|
23
|
+
// Shared files
|
|
24
|
+
if (data.sharedCss) {
|
|
25
|
+
files.push({ id: "shared/theme.css", label: "theme.css", group: "Shared", lang: "css", content: data.sharedCss, shared: "css" });
|
|
26
|
+
}
|
|
27
|
+
if (data.sharedJs) {
|
|
28
|
+
files.push({ id: "shared/animations.js", label: "animations.js", group: "Shared", lang: "javascript", content: data.sharedJs, shared: "js" });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Per-module files
|
|
32
|
+
for (const mod of (data.modules || [])) {
|
|
33
|
+
const name = mod.moduleName;
|
|
34
|
+
files.push({ id: name + "/module.html", label: "module.html", group: name, lang: "html", content: mod.moduleHtml, moduleName: name, fileType: "html" });
|
|
35
|
+
files.push({ id: name + "/module.css", label: "module.css", group: name, lang: "css", content: mod.moduleCss, moduleName: name, fileType: "css" });
|
|
36
|
+
if (mod.moduleJs) {
|
|
37
|
+
files.push({ id: name + "/module.js", label: "module.js", group: name, lang: "javascript", content: mod.moduleJs, moduleName: name, fileType: "js" });
|
|
38
|
+
}
|
|
39
|
+
files.push({ id: name + "/fields.json", label: "fields.json", group: name, lang: "json", content: mod.fieldsJson, moduleName: name, fileType: "fields" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return files;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// File browser rendering
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const FILE_ICONS = {
|
|
50
|
+
html: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
51
|
+
css: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M4 12h10M4 17h6"/></svg>',
|
|
52
|
+
javascript: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/></svg>',
|
|
53
|
+
json: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3"/><path d="M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3"/></svg>',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function renderCodeFileList(files) {
|
|
57
|
+
const listEl = document.getElementById("code-file-list");
|
|
58
|
+
listEl.innerHTML = "";
|
|
59
|
+
|
|
60
|
+
// Group files
|
|
61
|
+
const groups = new Map();
|
|
62
|
+
for (const f of files) {
|
|
63
|
+
if (!groups.has(f.group)) groups.set(f.group, []);
|
|
64
|
+
groups.get(f.group).push(f);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const [groupName, groupFiles] of groups) {
|
|
68
|
+
const groupEl = document.createElement("div");
|
|
69
|
+
groupEl.className = "code-view__file-group";
|
|
70
|
+
|
|
71
|
+
const headerEl = document.createElement("div");
|
|
72
|
+
headerEl.className = "code-view__group-header";
|
|
73
|
+
headerEl.textContent = groupName;
|
|
74
|
+
groupEl.appendChild(headerEl);
|
|
75
|
+
|
|
76
|
+
for (const f of groupFiles) {
|
|
77
|
+
const itemEl = document.createElement("div");
|
|
78
|
+
itemEl.className = "code-view__file-item";
|
|
79
|
+
itemEl.dataset.fileId = f.id;
|
|
80
|
+
itemEl.innerHTML = (FILE_ICONS[f.lang] || "") + '<span class="code-view__file-name">' + f.label + "</span>";
|
|
81
|
+
itemEl.addEventListener("click", () => openCodeFile(f));
|
|
82
|
+
groupEl.appendChild(itemEl);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
listEl.appendChild(groupEl);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Editor management
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Custom vibespot theme (uses CSS variables for dark/light support)
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function vibespotTheme() {
|
|
98
|
+
const theme = CM.EditorView.theme({
|
|
99
|
+
"&": {
|
|
100
|
+
backgroundColor: "var(--bg-deep, #0a0a0a)",
|
|
101
|
+
color: "var(--text, #f5f0eb)",
|
|
102
|
+
},
|
|
103
|
+
".cm-content": {
|
|
104
|
+
caretColor: "var(--accent, #e8613a)",
|
|
105
|
+
},
|
|
106
|
+
".cm-cursor, .cm-dropCursor": {
|
|
107
|
+
borderLeftColor: "var(--accent, #e8613a)",
|
|
108
|
+
},
|
|
109
|
+
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
|
110
|
+
backgroundColor: "var(--accent-dim, rgba(232,97,58,0.15))",
|
|
111
|
+
},
|
|
112
|
+
".cm-panels": {
|
|
113
|
+
backgroundColor: "var(--bg-panel-solid, #131110)",
|
|
114
|
+
color: "var(--text, #f5f0eb)",
|
|
115
|
+
},
|
|
116
|
+
".cm-panels.cm-panels-top": {
|
|
117
|
+
borderBottom: "1px solid var(--border, rgba(255,255,255,0.06))",
|
|
118
|
+
},
|
|
119
|
+
".cm-panels.cm-panels-bottom": {
|
|
120
|
+
borderTop: "1px solid var(--border, rgba(255,255,255,0.06))",
|
|
121
|
+
},
|
|
122
|
+
".cm-searchMatch": {
|
|
123
|
+
backgroundColor: "var(--accent-dim, rgba(232,97,58,0.15))",
|
|
124
|
+
outline: "1px solid var(--accent-glow, rgba(232,97,58,0.3))",
|
|
125
|
+
},
|
|
126
|
+
".cm-searchMatch.cm-searchMatch-selected": {
|
|
127
|
+
backgroundColor: "var(--accent-dim, rgba(232,97,58,0.15))",
|
|
128
|
+
},
|
|
129
|
+
".cm-activeLine": {
|
|
130
|
+
backgroundColor: "var(--bg-input, rgba(255,255,255,0.04))",
|
|
131
|
+
},
|
|
132
|
+
".cm-selectionMatch": {
|
|
133
|
+
backgroundColor: "var(--accent-tint, rgba(232,97,58,0.06))",
|
|
134
|
+
},
|
|
135
|
+
".cm-matchingBracket, .cm-nonmatchingBracket": {
|
|
136
|
+
backgroundColor: "var(--accent-dim, rgba(232,97,58,0.15))",
|
|
137
|
+
outline: "1px solid var(--accent-glow, rgba(232,97,58,0.3))",
|
|
138
|
+
},
|
|
139
|
+
".cm-gutters": {
|
|
140
|
+
backgroundColor: "var(--bg-panel-solid, #131110)",
|
|
141
|
+
color: "var(--text-muted, rgba(255,255,255,0.2))",
|
|
142
|
+
border: "none",
|
|
143
|
+
borderRight: "1px solid var(--border, rgba(255,255,255,0.06))",
|
|
144
|
+
},
|
|
145
|
+
".cm-activeLineGutter": {
|
|
146
|
+
backgroundColor: "var(--bg-input, rgba(255,255,255,0.04))",
|
|
147
|
+
color: "var(--text-dim, rgba(255,255,255,0.45))",
|
|
148
|
+
},
|
|
149
|
+
".cm-foldPlaceholder": {
|
|
150
|
+
backgroundColor: "transparent",
|
|
151
|
+
border: "none",
|
|
152
|
+
color: "var(--text-muted)",
|
|
153
|
+
},
|
|
154
|
+
".cm-tooltip": {
|
|
155
|
+
border: "1px solid var(--border)",
|
|
156
|
+
backgroundColor: "var(--bg-panel-solid)",
|
|
157
|
+
color: "var(--text)",
|
|
158
|
+
},
|
|
159
|
+
".cm-tooltip .cm-tooltip-arrow:before": {
|
|
160
|
+
borderTopColor: "transparent",
|
|
161
|
+
borderBottomColor: "transparent",
|
|
162
|
+
},
|
|
163
|
+
".cm-tooltip .cm-tooltip-arrow:after": {
|
|
164
|
+
borderTopColor: "var(--bg-panel-solid)",
|
|
165
|
+
borderBottomColor: "var(--bg-panel-solid)",
|
|
166
|
+
},
|
|
167
|
+
".cm-tooltip-autocomplete": {
|
|
168
|
+
"& > ul > li[aria-selected]": {
|
|
169
|
+
backgroundColor: "var(--accent-dim)",
|
|
170
|
+
color: "var(--text)",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}, { dark: true });
|
|
174
|
+
|
|
175
|
+
return theme;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function vibespotHighlight() {
|
|
179
|
+
const t = CM.tags;
|
|
180
|
+
return CM.syntaxHighlighting(CM.HighlightStyle.define([
|
|
181
|
+
{ tag: t.keyword, color: "#c678dd" },
|
|
182
|
+
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: "#e06c75" },
|
|
183
|
+
{ tag: [t.function(t.variableName), t.labelName], color: "#61afef" },
|
|
184
|
+
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: "#d19a66" },
|
|
185
|
+
{ tag: [t.definition(t.name), t.separator], color: "#abb2bf" },
|
|
186
|
+
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: "#e5c07b" },
|
|
187
|
+
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: "#56b6c2" },
|
|
188
|
+
{ tag: [t.meta, t.comment], color: "#5c6370", fontStyle: "italic" },
|
|
189
|
+
{ tag: t.strong, fontWeight: "bold" },
|
|
190
|
+
{ tag: t.emphasis, fontStyle: "italic" },
|
|
191
|
+
{ tag: t.strikethrough, textDecoration: "line-through" },
|
|
192
|
+
{ tag: t.link, color: "#56b6c2", textDecoration: "underline" },
|
|
193
|
+
{ tag: t.heading, fontWeight: "bold", color: "#e06c75" },
|
|
194
|
+
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: "#d19a66" },
|
|
195
|
+
{ tag: [t.processingInstruction, t.string, t.inserted], color: "#98c379" },
|
|
196
|
+
{ tag: t.invalid, color: "#ffffff", backgroundColor: "#e06c75" },
|
|
197
|
+
]));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getLangExtension(lang) {
|
|
201
|
+
if (!window.CM) return [];
|
|
202
|
+
switch (lang) {
|
|
203
|
+
case "html": return [CM.html()];
|
|
204
|
+
case "css": return [CM.css()];
|
|
205
|
+
case "javascript": return [CM.javascript()];
|
|
206
|
+
case "json": return [CM.json()];
|
|
207
|
+
default: return [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function openCodeFile(file) {
|
|
212
|
+
// Warn if dirty
|
|
213
|
+
if (codeDirty && currentCodeFile) {
|
|
214
|
+
if (!confirm("You have unsaved changes. Discard them?")) return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
currentCodeFile = file;
|
|
218
|
+
codeDirty = false;
|
|
219
|
+
updateDirtyState();
|
|
220
|
+
|
|
221
|
+
// Update sidebar selection
|
|
222
|
+
document.querySelectorAll(".code-view__file-item").forEach((el) => {
|
|
223
|
+
el.classList.toggle("code-view__file-item--active", el.dataset.fileId === file.id);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Update header
|
|
227
|
+
document.getElementById("code-filename").textContent = file.id;
|
|
228
|
+
|
|
229
|
+
// Destroy previous editor
|
|
230
|
+
if (codeEditorView) {
|
|
231
|
+
codeEditorView.destroy();
|
|
232
|
+
codeEditorView = null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const area = document.getElementById("code-editor-area");
|
|
236
|
+
area.innerHTML = "";
|
|
237
|
+
|
|
238
|
+
if (!window.CM) {
|
|
239
|
+
area.textContent = "CodeMirror not loaded";
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const saveKeymap = CM.keymap.of([{
|
|
244
|
+
key: "Mod-s",
|
|
245
|
+
run: () => { saveCurrentFile(); return true; },
|
|
246
|
+
}]);
|
|
247
|
+
|
|
248
|
+
codeEditorView = new CM.EditorView({
|
|
249
|
+
state: CM.EditorState.create({
|
|
250
|
+
doc: file.lang === "json" ? prettyJson(file.content) : (file.content || ""),
|
|
251
|
+
extensions: [
|
|
252
|
+
CM.basicSetup,
|
|
253
|
+
...getLangExtension(file.lang),
|
|
254
|
+
vibespotTheme(),
|
|
255
|
+
vibespotHighlight(),
|
|
256
|
+
saveKeymap,
|
|
257
|
+
CM.EditorView.updateListener.of((update) => {
|
|
258
|
+
if (update.docChanged && !codeDirty) {
|
|
259
|
+
codeDirty = true;
|
|
260
|
+
updateDirtyState();
|
|
261
|
+
}
|
|
262
|
+
}),
|
|
263
|
+
],
|
|
264
|
+
}),
|
|
265
|
+
parent: area,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function updateDirtyState() {
|
|
270
|
+
const dot = document.getElementById("code-dirty-dot");
|
|
271
|
+
const btn = document.getElementById("code-save-btn");
|
|
272
|
+
if (codeDirty) {
|
|
273
|
+
dot.classList.remove("hidden");
|
|
274
|
+
btn.disabled = false;
|
|
275
|
+
} else {
|
|
276
|
+
dot.classList.add("hidden");
|
|
277
|
+
btn.disabled = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Save
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
async function saveCurrentFile() {
|
|
286
|
+
if (!currentCodeFile || !codeEditorView || !codeDirty) return;
|
|
287
|
+
|
|
288
|
+
const content = codeEditorView.state.doc.toString();
|
|
289
|
+
const btn = document.getElementById("code-save-btn");
|
|
290
|
+
btn.disabled = true;
|
|
291
|
+
btn.textContent = "Saving...";
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const body = currentCodeFile.shared
|
|
295
|
+
? { shared: currentCodeFile.shared, content }
|
|
296
|
+
: { moduleName: currentCodeFile.moduleName, fileType: currentCodeFile.fileType, content };
|
|
297
|
+
|
|
298
|
+
const res = await fetch("/api/modules/code", {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: { "Content-Type": "application/json" },
|
|
301
|
+
body: JSON.stringify(body),
|
|
302
|
+
});
|
|
303
|
+
const data = await res.json();
|
|
304
|
+
if (data.ok) {
|
|
305
|
+
codeDirty = false;
|
|
306
|
+
codeSavedSinceSwitch = true;
|
|
307
|
+
updateDirtyState();
|
|
308
|
+
// Update the cached content so switching files doesn't lose changes
|
|
309
|
+
currentCodeFile.content = content;
|
|
310
|
+
// Refresh preview so changes are visible when switching back
|
|
311
|
+
if (typeof refreshPreview === "function") refreshPreview();
|
|
312
|
+
} else {
|
|
313
|
+
await vibeAlert(data.error || "Save failed", "Error");
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
await vibeAlert("Save failed: " + err.message, "Error");
|
|
317
|
+
} finally {
|
|
318
|
+
btn.textContent = "Save";
|
|
319
|
+
if (!codeDirty) btn.disabled = true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
document.getElementById("code-save-btn")?.addEventListener("click", saveCurrentFile);
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// View toggle
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
async function loadCodeFiles() {
|
|
330
|
+
try {
|
|
331
|
+
const res = await fetch("/api/modules");
|
|
332
|
+
const data = await res.json();
|
|
333
|
+
codeFiles = buildCodeFileTree(data);
|
|
334
|
+
renderCodeFileList(codeFiles);
|
|
335
|
+
|
|
336
|
+
// Auto-select first file if none selected
|
|
337
|
+
if (codeFiles.length > 0 && !currentCodeFile) {
|
|
338
|
+
openCodeFile(codeFiles[0]);
|
|
339
|
+
} else if (currentCodeFile) {
|
|
340
|
+
// Re-sync content from server
|
|
341
|
+
const updated = codeFiles.find((f) => f.id === currentCodeFile.id);
|
|
342
|
+
if (updated && !codeDirty) {
|
|
343
|
+
openCodeFile(updated);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} catch (err) {
|
|
347
|
+
console.warn("Failed to load code files:", err);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
document.querySelectorAll(".view-toggle__btn").forEach((btn) => {
|
|
352
|
+
btn.addEventListener("click", () => {
|
|
353
|
+
const view = btn.dataset.view;
|
|
354
|
+
document.querySelectorAll(".view-toggle__btn").forEach((b) => b.classList.remove("active"));
|
|
355
|
+
btn.classList.add("active");
|
|
356
|
+
|
|
357
|
+
const previewEl = document.getElementById("preview-container");
|
|
358
|
+
const codeEl = document.getElementById("code-view");
|
|
359
|
+
const chromeBar = document.getElementById("browser-chrome");
|
|
360
|
+
|
|
361
|
+
if (view === "code") {
|
|
362
|
+
previewEl.classList.add("hidden");
|
|
363
|
+
codeEl.classList.remove("hidden");
|
|
364
|
+
loadCodeFiles();
|
|
365
|
+
} else {
|
|
366
|
+
codeEl.classList.add("hidden");
|
|
367
|
+
previewEl.classList.remove("hidden");
|
|
368
|
+
// Refresh preview if code was saved while in code view
|
|
369
|
+
if (codeSavedSinceSwitch && typeof refreshPreview === "function") {
|
|
370
|
+
refreshPreview();
|
|
371
|
+
}
|
|
372
|
+
codeSavedSinceSwitch = false;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Reset code editor state when switching templates/themes
|
|
378
|
+
function resetCodeEditor() {
|
|
379
|
+
if (codeEditorView) {
|
|
380
|
+
codeEditorView.destroy();
|
|
381
|
+
codeEditorView = null;
|
|
382
|
+
}
|
|
383
|
+
currentCodeFile = null;
|
|
384
|
+
codeDirty = false;
|
|
385
|
+
codeFiles = [];
|
|
386
|
+
const listEl = document.getElementById("code-file-list");
|
|
387
|
+
if (listEl) listEl.innerHTML = "";
|
|
388
|
+
const area = document.getElementById("code-editor-area");
|
|
389
|
+
if (area) area.innerHTML = "";
|
|
390
|
+
const filename = document.getElementById("code-filename");
|
|
391
|
+
if (filename) filename.textContent = "Select a file";
|
|
392
|
+
updateDirtyState();
|
|
393
|
+
}
|
package/ui/dashboard.js
CHANGED
|
@@ -110,6 +110,7 @@ function renderTemplateList(templates) {
|
|
|
110
110
|
<span class="dashboard__template-label">${esc(tpl.label)}</span>
|
|
111
111
|
<span class="dashboard__template-meta">${tpl.moduleCount} module${tpl.moduleCount !== 1 ? "s" : ""}</span>
|
|
112
112
|
<button class="btn btn--sm btn--primary dashboard__template-open" data-id="${esc(tpl.id)}">Open</button>
|
|
113
|
+
<button class="dashboard__template-clone" data-id="${esc(tpl.id)}" title="Clone template">⧉</button>
|
|
113
114
|
<button class="dashboard__template-delete" data-id="${esc(tpl.id)}" title="Delete template">×</button>
|
|
114
115
|
`;
|
|
115
116
|
list.appendChild(item);
|
|
@@ -119,6 +120,8 @@ function renderTemplateList(templates) {
|
|
|
119
120
|
list.onclick = (e) => {
|
|
120
121
|
const openBtn = e.target.closest(".dashboard__template-open");
|
|
121
122
|
if (openBtn) return openTemplate(openBtn.dataset.id);
|
|
123
|
+
const cloneBtn = e.target.closest(".dashboard__template-clone");
|
|
124
|
+
if (cloneBtn) return cloneTemplateAction(cloneBtn.dataset.id);
|
|
122
125
|
const delBtn = e.target.closest(".dashboard__template-delete");
|
|
123
126
|
if (delBtn) return confirmDeleteTemplate(delBtn.dataset.id);
|
|
124
127
|
};
|
|
@@ -290,6 +293,31 @@ function renderBrandAssets(assets) {
|
|
|
290
293
|
bvIcon.classList.remove("brand-asset-upload__icon--done");
|
|
291
294
|
}
|
|
292
295
|
|
|
296
|
+
// View buttons — show only when asset exists
|
|
297
|
+
let sgView = document.getElementById("btn-view-styleguide");
|
|
298
|
+
if (!sgView) {
|
|
299
|
+
sgView = document.createElement("button");
|
|
300
|
+
sgView.id = "btn-view-styleguide";
|
|
301
|
+
sgView.className = "btn btn--sm btn--ghost brand-asset-view";
|
|
302
|
+
sgView.textContent = "View";
|
|
303
|
+
sgView.title = "View styleguide";
|
|
304
|
+
sgView.addEventListener("click", viewStyleguide);
|
|
305
|
+
document.getElementById("brand-upload-styleguide")?.after(sgView);
|
|
306
|
+
}
|
|
307
|
+
sgView.style.display = assets.hasStyleguide ? "" : "none";
|
|
308
|
+
|
|
309
|
+
let bvView = document.getElementById("btn-view-brandvoice");
|
|
310
|
+
if (!bvView) {
|
|
311
|
+
bvView = document.createElement("button");
|
|
312
|
+
bvView.id = "btn-view-brandvoice";
|
|
313
|
+
bvView.className = "btn btn--sm btn--ghost brand-asset-view";
|
|
314
|
+
bvView.textContent = "View";
|
|
315
|
+
bvView.title = "View brand voice";
|
|
316
|
+
bvView.addEventListener("click", viewBrandvoice);
|
|
317
|
+
document.getElementById("brand-upload-brandvoice")?.after(bvView);
|
|
318
|
+
}
|
|
319
|
+
bvView.style.display = assets.hasBrandvoice ? "" : "none";
|
|
320
|
+
|
|
293
321
|
// Humanify toggle
|
|
294
322
|
const humanifyCheckbox = document.getElementById("humanify-checkbox");
|
|
295
323
|
if (humanifyCheckbox) {
|
|
@@ -297,6 +325,34 @@ function renderBrandAssets(assets) {
|
|
|
297
325
|
}
|
|
298
326
|
}
|
|
299
327
|
|
|
328
|
+
async function viewStyleguide() {
|
|
329
|
+
try {
|
|
330
|
+
const res = await fetch("/api/brand-assets");
|
|
331
|
+
const data = await res.json();
|
|
332
|
+
if (data.styleguide) {
|
|
333
|
+
await vibeViewContent(data.styleguide, "Styleguide", "styleguide.md");
|
|
334
|
+
} else {
|
|
335
|
+
await vibeAlert("No styleguide found.", "Info");
|
|
336
|
+
}
|
|
337
|
+
} catch (err) {
|
|
338
|
+
await vibeAlert("Failed to load styleguide: " + err.message, "Error");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function viewBrandvoice() {
|
|
343
|
+
try {
|
|
344
|
+
const res = await fetch("/api/brand-assets");
|
|
345
|
+
const data = await res.json();
|
|
346
|
+
if (data.brandvoice) {
|
|
347
|
+
await vibeViewContent(data.brandvoice, "Brand Voice", "brandvoice.md");
|
|
348
|
+
} else {
|
|
349
|
+
await vibeAlert("No brand voice found.", "Info");
|
|
350
|
+
}
|
|
351
|
+
} catch (err) {
|
|
352
|
+
await vibeAlert("Failed to load brand voice: " + err.message, "Error");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
300
356
|
// ---------------------------------------------------------------------------
|
|
301
357
|
// Actions
|
|
302
358
|
// ---------------------------------------------------------------------------
|
|
@@ -368,6 +424,27 @@ async function confirmDeleteTemplate(templateId) {
|
|
|
368
424
|
}
|
|
369
425
|
}
|
|
370
426
|
|
|
427
|
+
async function cloneTemplateAction(templateId) {
|
|
428
|
+
const label = prompt("Name for the cloned template:");
|
|
429
|
+
if (!label) return;
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const res = await fetch("/api/templates/clone", {
|
|
433
|
+
method: "POST",
|
|
434
|
+
headers: { "Content-Type": "application/json" },
|
|
435
|
+
body: JSON.stringify({ templateId, label }),
|
|
436
|
+
});
|
|
437
|
+
const data = await res.json();
|
|
438
|
+
if (!data.ok) {
|
|
439
|
+
await vibeAlert(data.error || "Clone failed", "Error");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
await refreshDashboard();
|
|
443
|
+
} catch (err) {
|
|
444
|
+
await vibeAlert("Failed to clone: " + err.message, "Error");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
371
448
|
async function uploadBrandAsset(type) {
|
|
372
449
|
const uploadEl = document.getElementById(`brand-upload-${type}`);
|
|
373
450
|
const fileInput = uploadEl.querySelector("input[type=file]");
|
|
@@ -468,6 +545,81 @@ document.getElementById("brand-upload-brandvoice").querySelector("input").addEve
|
|
|
468
545
|
if (e.target.files[0]) handleBrandFileSelected("brandvoice", e.target.files[0]);
|
|
469
546
|
});
|
|
470
547
|
|
|
548
|
+
// Extract design from theme
|
|
549
|
+
document.getElementById("btn-extract-design")?.addEventListener("click", async () => {
|
|
550
|
+
const btn = document.getElementById("btn-extract-design");
|
|
551
|
+
const origText = btn.textContent;
|
|
552
|
+
btn.textContent = "Extracting...";
|
|
553
|
+
btn.disabled = true;
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const res = await fetch("/api/brand-assets/extract", {
|
|
557
|
+
method: "POST",
|
|
558
|
+
headers: { "Content-Type": "application/json" },
|
|
559
|
+
body: JSON.stringify({}),
|
|
560
|
+
});
|
|
561
|
+
const data = await res.json();
|
|
562
|
+
if (data.ok) {
|
|
563
|
+
await refreshDashboard();
|
|
564
|
+
const view = await vibeConfirm("Design system extracted and saved as styleguide.", "Would you like to view it?", { confirmLabel: "View Styleguide", confirmClass: "btn--primary" });
|
|
565
|
+
if (view && data.styleguide) {
|
|
566
|
+
await vibeViewContent(data.styleguide, "Styleguide", "styleguide.md");
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
await vibeAlert(data.error || "Extraction failed", "Error");
|
|
570
|
+
}
|
|
571
|
+
} catch (err) {
|
|
572
|
+
await vibeAlert("Extraction failed: " + err.message, "Error");
|
|
573
|
+
} finally {
|
|
574
|
+
btn.textContent = origText;
|
|
575
|
+
btn.disabled = false;
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Import design reference from another theme
|
|
580
|
+
document.getElementById("btn-import-reference")?.addEventListener("click", async () => {
|
|
581
|
+
const input = await vibePrompt(
|
|
582
|
+
"Import design from another theme",
|
|
583
|
+
"",
|
|
584
|
+
"HubSpot theme name or local path (e.g. ~/vibespot-themes/my-theme)"
|
|
585
|
+
);
|
|
586
|
+
if (!input) return;
|
|
587
|
+
|
|
588
|
+
// Detect source: if it looks like a path (contains / or ~), treat as local
|
|
589
|
+
const isLocal = input.includes("/") || input.startsWith("~");
|
|
590
|
+
const body = isLocal
|
|
591
|
+
? { source: "local", localPath: input }
|
|
592
|
+
: { source: "hubspot", themeName: input };
|
|
593
|
+
|
|
594
|
+
const btn = document.getElementById("btn-import-reference");
|
|
595
|
+
const origText = btn.textContent;
|
|
596
|
+
btn.textContent = "Importing...";
|
|
597
|
+
btn.disabled = true;
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
const res = await fetch("/api/brand-assets/import-reference", {
|
|
601
|
+
method: "POST",
|
|
602
|
+
headers: { "Content-Type": "application/json" },
|
|
603
|
+
body: JSON.stringify(body),
|
|
604
|
+
});
|
|
605
|
+
const data = await res.json();
|
|
606
|
+
if (data.ok) {
|
|
607
|
+
await refreshDashboard();
|
|
608
|
+
const view = await vibeConfirm("Design imported and saved as styleguide.", "Would you like to view it?", { confirmLabel: "View Styleguide", confirmClass: "btn--primary" });
|
|
609
|
+
if (view && data.styleguide) {
|
|
610
|
+
await vibeViewContent(data.styleguide, "Styleguide", "styleguide.md");
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
await vibeAlert(data.error || "Import failed", "Error");
|
|
614
|
+
}
|
|
615
|
+
} catch (err) {
|
|
616
|
+
await vibeAlert("Import failed: " + err.message, "Error");
|
|
617
|
+
} finally {
|
|
618
|
+
btn.textContent = origText;
|
|
619
|
+
btn.disabled = false;
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
471
623
|
// Dashboard theme heading — double-click to rename
|
|
472
624
|
document.getElementById("dashboard-theme-heading")?.addEventListener("dblclick", () => {
|
|
473
625
|
const el = document.getElementById("dashboard-theme-heading");
|