vibespot 1.1.1 → 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/LICENSE +103 -33
- package/README.md +55 -6
- 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/agency-services.md +42 -0
- package/assets/plan-templates/blog-content-hub.md +50 -0
- package/assets/plan-templates/ecommerce-product.md +42 -0
- 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/assets/plan-templates/event-registration.md +42 -0
- package/assets/plan-templates/portfolio.md +41 -0
- package/assets/plan-templates/restaurant.md +42 -0
- package/assets/plan-templates/saas-landing.md +42 -0
- package/dist/index.js +1485 -397
- package/dist/index.js.map +1 -1
- package/package.json +11 -7
- package/starters/01-saas-landing.json +43 -0
- package/starters/02-portfolio.json +39 -0
- package/starters/03-restaurant.json +39 -0
- package/starters/04-event.json +39 -0
- package/starters/05-coming-soon.json +32 -0
- 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 +1604 -155
- package/ui/code-editor.js +49 -7
- package/ui/dashboard.js +551 -83
- package/ui/docs/docs.css +29 -0
- package/ui/docs/index.html +274 -117
- 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 +71 -0
- package/ui/icons.js +120 -0
- package/ui/index.html +882 -515
- package/ui/inline-edit.js +710 -0
- package/ui/marketplace.js +218 -0
- package/ui/plan.js +0 -0
- package/ui/preview.js +219 -1
- package/ui/section-controls.js +628 -0
- package/ui/settings.js +84 -28
- package/ui/setup.js +1016 -118
- package/ui/styles.css +6119 -2456
- package/ui/upload-panel.js +47 -20
package/ui/dashboard.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Sits between setup (project list) and chat (template editing).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const dashboardScreen = document.getElementById("
|
|
6
|
+
const dashboardScreen = document.getElementById("editor");
|
|
7
7
|
|
|
8
8
|
// Page type labels for display
|
|
9
9
|
const PAGE_TYPE_LABELS = {
|
|
@@ -26,27 +26,29 @@ const PAGE_TYPE_FULL_LABELS = {
|
|
|
26
26
|
|
|
27
27
|
let currentDashboardTheme = "";
|
|
28
28
|
let currentDashboardSessionId = "";
|
|
29
|
+
let currentDashboardIsImported = false;
|
|
29
30
|
|
|
30
31
|
async function showDashboard(themeName) {
|
|
31
32
|
currentDashboardTheme = themeName;
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
document.getElementById("
|
|
36
|
-
document.getElementById("project-rail")?.classList.remove("project-rail--expanded");
|
|
37
|
-
appScreen.classList.add("hidden");
|
|
34
|
+
const ab = document.getElementById("app-body");
|
|
35
|
+
if (ab) ab.dataset.mode = "editor";
|
|
36
|
+
document.getElementById("project-rail")?.setAttribute("data-mode", "editor");
|
|
38
37
|
dashboardScreen.classList.remove("hidden");
|
|
39
38
|
|
|
40
|
-
document.getElementById("
|
|
41
|
-
|
|
42
|
-
document.getElementById("dashboard-theme-path-text").textContent = "";
|
|
39
|
+
document.getElementById("theme-name").textContent = themeName;
|
|
40
|
+
if (typeof updateRailActive === "function") updateRailActive();
|
|
43
41
|
|
|
44
42
|
// Get sessionId for the active theme
|
|
45
43
|
try {
|
|
46
44
|
const themesRes = await fetch("/api/themes");
|
|
47
45
|
const themesData = await themesRes.json();
|
|
48
46
|
currentDashboardSessionId = themesData.activeTheme?.id || "";
|
|
49
|
-
|
|
47
|
+
currentDashboardIsImported = !!themesData.activeTheme?.isImported;
|
|
48
|
+
} catch {
|
|
49
|
+
currentDashboardSessionId = "";
|
|
50
|
+
currentDashboardIsImported = false;
|
|
51
|
+
}
|
|
50
52
|
|
|
51
53
|
// Update URL
|
|
52
54
|
const target = "#/dashboard/" + encodeURIComponent(themeName);
|
|
@@ -56,11 +58,18 @@ async function showDashboard(themeName) {
|
|
|
56
58
|
|
|
57
59
|
// Load dashboard data
|
|
58
60
|
await refreshDashboard();
|
|
61
|
+
|
|
62
|
+
// Establish WebSocket so the page tree (populated via WS init message)
|
|
63
|
+
// and chat input work immediately — without this, a browser refresh
|
|
64
|
+
// leaves the page tree empty and the chat send button inert.
|
|
65
|
+
if (typeof connectWebSocket === "function") {
|
|
66
|
+
connectWebSocket();
|
|
67
|
+
}
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
function hideDashboard() {
|
|
62
|
-
dashboardScreen.classList.add("hidden");
|
|
63
71
|
currentDashboardTheme = "";
|
|
72
|
+
currentDashboardIsImported = false;
|
|
64
73
|
closeModulePreview();
|
|
65
74
|
}
|
|
66
75
|
|
|
@@ -77,16 +86,196 @@ async function refreshDashboard() {
|
|
|
77
86
|
return;
|
|
78
87
|
}
|
|
79
88
|
renderTemplateList(data.templates || []);
|
|
89
|
+
renderProjectAssets(data.templates || []);
|
|
80
90
|
renderModuleLibrary(data.moduleLibrary || []);
|
|
81
91
|
renderBrandAssets(data.brandAssets || {});
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
await loadFontList();
|
|
93
|
+
renderBrandKit(data.brandAssets?.brandKit || null);
|
|
94
|
+
const pathEl = document.getElementById("dashboard-theme-path-text");
|
|
95
|
+
if (data.themePath && pathEl) {
|
|
96
|
+
pathEl.textContent = data.themePath;
|
|
97
|
+
}
|
|
98
|
+
if (currentDashboardIsImported) {
|
|
99
|
+
await refreshInverseAnalysis();
|
|
100
|
+
} else {
|
|
101
|
+
hideInverseAnalysis();
|
|
84
102
|
}
|
|
85
103
|
} catch (err) {
|
|
86
104
|
console.error("Failed to load dashboard:", err);
|
|
87
105
|
}
|
|
88
106
|
}
|
|
89
107
|
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Import analysis
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
function hideInverseAnalysis() {
|
|
113
|
+
const section = document.getElementById("dashboard-inverse-section");
|
|
114
|
+
const summaryEl = document.getElementById("inverse-summary");
|
|
115
|
+
const status = document.getElementById("inverse-status");
|
|
116
|
+
const applyBtn = document.getElementById("btn-inverse-apply-tokens");
|
|
117
|
+
section?.classList.add("hidden");
|
|
118
|
+
if (summaryEl) summaryEl.innerHTML = "";
|
|
119
|
+
if (status) status.textContent = "Analyzing theme...";
|
|
120
|
+
if (applyBtn) applyBtn.classList.add("hidden");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function refreshInverseAnalysis() {
|
|
124
|
+
const section = document.getElementById("dashboard-inverse-section");
|
|
125
|
+
const status = document.getElementById("inverse-status");
|
|
126
|
+
const summaryEl = document.getElementById("inverse-summary");
|
|
127
|
+
const applyBtn = document.getElementById("btn-inverse-apply-tokens");
|
|
128
|
+
if (!section || !summaryEl) return;
|
|
129
|
+
|
|
130
|
+
const capturedSessionId = currentDashboardSessionId;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const res = await fetch("/api/inverse/analyze");
|
|
134
|
+
if (currentDashboardSessionId !== capturedSessionId) return;
|
|
135
|
+
const data = await res.json();
|
|
136
|
+
if (!res.ok || data.error) {
|
|
137
|
+
section.classList.add("hidden");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
section.classList.remove("hidden");
|
|
141
|
+
renderInverseAnalysis(data.report);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.warn("Import analysis failed:", err);
|
|
144
|
+
section.classList.add("hidden");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderInverseAnalysis(report) {
|
|
149
|
+
const status = document.getElementById("inverse-status");
|
|
150
|
+
const summaryEl = document.getElementById("inverse-summary");
|
|
151
|
+
const applyBtn = document.getElementById("btn-inverse-apply-tokens");
|
|
152
|
+
if (!report || !summaryEl) return;
|
|
153
|
+
|
|
154
|
+
const counts = report.summary || {};
|
|
155
|
+
const tokens = report.designTokens || {};
|
|
156
|
+
const findings = report.findings || [];
|
|
157
|
+
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
158
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
159
|
+
const hasInferredTokens = (tokens.palette || []).length > 0;
|
|
160
|
+
const hasCssVars = (counts.cssVarCount || 0) > 0;
|
|
161
|
+
|
|
162
|
+
if (status) {
|
|
163
|
+
if (errors > 0) status.textContent = `${errors} issue${errors === 1 ? "" : "s"} need attention`;
|
|
164
|
+
else if (warnings > 0) status.textContent = `${warnings} warning${warnings === 1 ? "" : "s"}`;
|
|
165
|
+
else status.textContent = "No blocking risks found";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (applyBtn) {
|
|
169
|
+
applyBtn.classList.toggle("hidden", hasCssVars || !hasInferredTokens);
|
|
170
|
+
applyBtn.disabled = false;
|
|
171
|
+
applyBtn.textContent = "Apply Tokens";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const stats = [
|
|
175
|
+
["Modules (disk)", counts.moduleCount || 0],
|
|
176
|
+
["Templates (disk)", counts.templateCount || 0],
|
|
177
|
+
["Orphans", counts.orphanCount || 0],
|
|
178
|
+
["Palette", counts.paletteSize || 0],
|
|
179
|
+
["CSS Vars", counts.cssVarCount || 0],
|
|
180
|
+
["Macros", counts.customMacroCount || 0],
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
let html = `<div class="inverse-summary__stats">`;
|
|
184
|
+
for (const [label, value] of stats) {
|
|
185
|
+
html += `
|
|
186
|
+
<div class="inverse-stat">
|
|
187
|
+
<span class="inverse-stat__value">${esc(String(value))}</span>
|
|
188
|
+
<span class="inverse-stat__label">${esc(label)}</span>
|
|
189
|
+
</div>
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
html += `</div>`;
|
|
193
|
+
|
|
194
|
+
if ((tokens.palette || []).length > 0) {
|
|
195
|
+
html += `<div class="inverse-block"><div class="inverse-block__label">Palette</div><div class="inverse-swatches">`;
|
|
196
|
+
for (const color of tokens.palette.slice(0, 8)) {
|
|
197
|
+
const label = color.varName ? `${color.value} (${color.varName})` : color.value;
|
|
198
|
+
html += `<span class="inverse-swatch" style="background:${inverseCssColor(color.value)}" title="${inverseEscAttr(label)}"></span>`;
|
|
199
|
+
}
|
|
200
|
+
html += `</div></div>`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if ((tokens.fontFamilies || []).length > 0) {
|
|
204
|
+
html += `<div class="inverse-block"><div class="inverse-block__label">Typography</div><div class="inverse-tags">`;
|
|
205
|
+
for (const font of tokens.fontFamilies.slice(0, 4)) {
|
|
206
|
+
html += `<span class="inverse-tag">${esc(font)}</span>`;
|
|
207
|
+
}
|
|
208
|
+
html += `</div></div>`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
html += renderInverseFindings(findings);
|
|
212
|
+
summaryEl.innerHTML = html;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderInverseFindings(findings) {
|
|
216
|
+
if (!findings || findings.length === 0) {
|
|
217
|
+
return `<div class="inverse-findings inverse-findings--empty">No findings. This imported theme looks straightforward to edit.</div>`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
221
|
+
const sorted = [...findings].sort((a, b) => (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2));
|
|
222
|
+
const visible = sorted.slice(0, 5);
|
|
223
|
+
let html = `<div class="inverse-findings">`;
|
|
224
|
+
for (const finding of visible) {
|
|
225
|
+
const severity = ["error", "warning", "info"].includes(finding.severity) ? finding.severity : "info";
|
|
226
|
+
const fixAttr = finding.fix ? ` title="${inverseEscAttr(finding.fix)}"` : "";
|
|
227
|
+
html += `
|
|
228
|
+
<div class="inverse-finding inverse-finding--${severity}">
|
|
229
|
+
<span class="inverse-finding__severity">${esc(severity)}</span>
|
|
230
|
+
<span class="inverse-finding__message"${fixAttr}>${esc(finding.message)}</span>
|
|
231
|
+
</div>
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
if (findings.length > visible.length) {
|
|
235
|
+
html += `<div class="inverse-findings__more">${findings.length - visible.length} more finding${findings.length - visible.length === 1 ? "" : "s"} available in the CLI report.</div>`;
|
|
236
|
+
}
|
|
237
|
+
html += `</div>`;
|
|
238
|
+
return html;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function inverseEscAttr(value) {
|
|
242
|
+
return esc(String(value)).replace(/"/g, """).replace(/'/g, "'");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function inverseCssColor(value) {
|
|
246
|
+
const color = String(value || "").trim();
|
|
247
|
+
if (/^#[0-9a-fA-F]{3,8}$/.test(color)) return color;
|
|
248
|
+
if (/^rgba?\([0-9.,%\s]+\)$/.test(color)) return color;
|
|
249
|
+
if (/^hsla?\([0-9.,%\sdegturnrad+-]+\)$/.test(color)) return color;
|
|
250
|
+
return "transparent";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
document.getElementById("btn-inverse-apply-tokens")?.addEventListener("click", async () => {
|
|
254
|
+
const btn = document.getElementById("btn-inverse-apply-tokens");
|
|
255
|
+
if (!btn) return;
|
|
256
|
+
|
|
257
|
+
btn.disabled = true;
|
|
258
|
+
btn.textContent = "Applying...";
|
|
259
|
+
try {
|
|
260
|
+
const res = await fetch("/api/inverse/apply-tokens", { method: "POST" });
|
|
261
|
+
const data = await res.json();
|
|
262
|
+
if (!res.ok || data.error) {
|
|
263
|
+
await vibeAlert(data.error || "Failed to apply tokens.", "Error");
|
|
264
|
+
btn.disabled = false;
|
|
265
|
+
btn.textContent = "Apply Tokens";
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (!data.applied) {
|
|
269
|
+
await vibeAlert(data.reason || "No tokens were applied.", "Info");
|
|
270
|
+
}
|
|
271
|
+
await refreshInverseAnalysis();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
await vibeAlert("Failed to apply tokens: " + err.message, "Error");
|
|
274
|
+
btn.disabled = false;
|
|
275
|
+
btn.textContent = "Apply Tokens";
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
90
279
|
// ---------------------------------------------------------------------------
|
|
91
280
|
// Template list
|
|
92
281
|
// ---------------------------------------------------------------------------
|
|
@@ -94,7 +283,8 @@ async function refreshDashboard() {
|
|
|
94
283
|
function renderTemplateList(templates) {
|
|
95
284
|
const list = document.getElementById("dashboard-template-list");
|
|
96
285
|
const countEl = document.getElementById("dashboard-template-count");
|
|
97
|
-
|
|
286
|
+
if (!list) return;
|
|
287
|
+
if (countEl) countEl.textContent = templates.length;
|
|
98
288
|
|
|
99
289
|
if (templates.length === 0) {
|
|
100
290
|
list.innerHTML = `<p class="dashboard__empty-state">No templates yet. Choose a page type above to get started.</p>`;
|
|
@@ -110,7 +300,7 @@ function renderTemplateList(templates) {
|
|
|
110
300
|
<span class="dashboard__template-label">${esc(tpl.label)}</span>
|
|
111
301
|
<span class="dashboard__template-meta">${tpl.moduleCount} module${tpl.moduleCount !== 1 ? "s" : ""}</span>
|
|
112
302
|
<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"
|
|
303
|
+
<button class="dashboard__template-clone" data-id="${esc(tpl.id)}" title="Clone template">${vsIcon("copy", {size: "sm"})}</button>
|
|
114
304
|
<button class="dashboard__template-delete" data-id="${esc(tpl.id)}" title="Delete template">×</button>
|
|
115
305
|
`;
|
|
116
306
|
list.appendChild(item);
|
|
@@ -193,6 +383,70 @@ function startTemplateRename(labelEl, templateId) {
|
|
|
193
383
|
});
|
|
194
384
|
}
|
|
195
385
|
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Project assets (Library tab)
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
const ASSET_TYPE_LABELS = {
|
|
391
|
+
landing_page: "Landing Page",
|
|
392
|
+
blog_post: "Blog Post",
|
|
393
|
+
website_page: "Website Page",
|
|
394
|
+
module_only: "Module Only",
|
|
395
|
+
email: "Email",
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
function renderProjectAssets(templates) {
|
|
399
|
+
const container = document.getElementById("library-assets-list");
|
|
400
|
+
if (!container) return;
|
|
401
|
+
|
|
402
|
+
if (!templates || templates.length === 0) {
|
|
403
|
+
container.innerHTML = `<p class="dashboard__empty-state">Assets will appear here as you create pages and emails.</p>`;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const pages = templates.filter((t) => t.pageType !== "email");
|
|
408
|
+
const emails = templates.filter((t) => t.pageType === "email");
|
|
409
|
+
|
|
410
|
+
let html = "";
|
|
411
|
+
|
|
412
|
+
if (pages.length > 0) {
|
|
413
|
+
html += `<div class="library-assets__group">`;
|
|
414
|
+
html += `<h3 class="library-assets__group-title">Pages <span class="library-assets__count">${pages.length}</span></h3>`;
|
|
415
|
+
for (const tpl of pages) {
|
|
416
|
+
html += renderAssetCard(tpl);
|
|
417
|
+
}
|
|
418
|
+
html += `</div>`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (emails.length > 0) {
|
|
422
|
+
html += `<div class="library-assets__group">`;
|
|
423
|
+
html += `<h3 class="library-assets__group-title">Emails <span class="library-assets__count">${emails.length}</span></h3>`;
|
|
424
|
+
for (const tpl of emails) {
|
|
425
|
+
html += renderAssetCard(tpl);
|
|
426
|
+
}
|
|
427
|
+
html += `</div>`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
container.innerHTML = html;
|
|
431
|
+
|
|
432
|
+
container.onclick = (e) => {
|
|
433
|
+
const card = e.target.closest(".library-asset-card");
|
|
434
|
+
if (!card) return;
|
|
435
|
+
const templateId = card.dataset.id;
|
|
436
|
+
if (templateId) openTemplate(templateId);
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function renderAssetCard(tpl) {
|
|
441
|
+
const typeLabel = ASSET_TYPE_LABELS[tpl.pageType] || tpl.pageType;
|
|
442
|
+
return `
|
|
443
|
+
<div class="library-asset-card" data-id="${esc(tpl.id)}" role="button" tabindex="0">
|
|
444
|
+
<span class="dashboard__template-badge dashboard__template-badge--${esc(tpl.pageType)}">${esc(PAGE_TYPE_LABELS[tpl.pageType] || typeLabel)}</span>
|
|
445
|
+
<span class="library-asset-card__label">${esc(tpl.label)}</span>
|
|
446
|
+
<span class="library-asset-card__meta">${tpl.moduleCount} module${tpl.moduleCount !== 1 ? "s" : ""}</span>
|
|
447
|
+
</div>`;
|
|
448
|
+
}
|
|
449
|
+
|
|
196
450
|
// ---------------------------------------------------------------------------
|
|
197
451
|
// Module library
|
|
198
452
|
// ---------------------------------------------------------------------------
|
|
@@ -201,6 +455,7 @@ let activePreviewModule = "";
|
|
|
201
455
|
|
|
202
456
|
function renderModuleLibrary(modules) {
|
|
203
457
|
const container = document.getElementById("dashboard-module-library");
|
|
458
|
+
if (!container) return;
|
|
204
459
|
|
|
205
460
|
if (modules.length === 0) {
|
|
206
461
|
container.innerHTML = `<p class="dashboard__empty-state">Modules will appear here as you build pages.</p>`;
|
|
@@ -260,17 +515,17 @@ async function showModulePreview(moduleName, usedIn) {
|
|
|
260
515
|
function closeModulePreview() {
|
|
261
516
|
activePreviewModule = "";
|
|
262
517
|
const previewEl = document.getElementById("dashboard-module-preview");
|
|
263
|
-
previewEl.classList.add("hidden");
|
|
518
|
+
if (previewEl) previewEl.classList.add("hidden");
|
|
264
519
|
document.querySelectorAll(".dashboard__module-chip").forEach((c) => {
|
|
265
520
|
c.classList.remove("dashboard__module-chip--active");
|
|
266
521
|
});
|
|
267
522
|
}
|
|
268
523
|
|
|
269
524
|
// Close button for module preview
|
|
270
|
-
document.getElementById("dashboard-preview-close")
|
|
525
|
+
document.getElementById("dashboard-preview-close")?.addEventListener("click", closeModulePreview);
|
|
271
526
|
|
|
272
527
|
// Delete button for module preview
|
|
273
|
-
document.getElementById("dashboard-preview-delete")
|
|
528
|
+
document.getElementById("dashboard-preview-delete")?.addEventListener("click", async () => {
|
|
274
529
|
const moduleName = activePreviewModule;
|
|
275
530
|
if (!moduleName) return;
|
|
276
531
|
|
|
@@ -377,9 +632,14 @@ async function extractBrandAsset(type, card) {
|
|
|
377
632
|
});
|
|
378
633
|
const data = await res.json();
|
|
379
634
|
if (data.ok && data.content) {
|
|
635
|
+
if (data.brandKit) {
|
|
636
|
+
await loadFontList();
|
|
637
|
+
renderBrandKit(data.brandKit);
|
|
638
|
+
}
|
|
380
639
|
await refreshDashboard();
|
|
640
|
+
const msg = data.brandKit ? `${ASSET_LABELS[type]} extracted. Brand kit updated from styleguide.` : `${ASSET_LABELS[type]} extracted.`;
|
|
381
641
|
const view = await vibeConfirm(
|
|
382
|
-
|
|
642
|
+
msg,
|
|
383
643
|
"Would you like to view it?",
|
|
384
644
|
{ confirmLabel: "View", confirmClass: "btn--primary" },
|
|
385
645
|
);
|
|
@@ -416,6 +676,220 @@ document.getElementById("dashboard-brand-assets")?.addEventListener("change", (e
|
|
|
416
676
|
handleBrandFileSelected(card.dataset.asset, e.target.files[0]);
|
|
417
677
|
});
|
|
418
678
|
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
// Brand kit
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
|
|
683
|
+
let _fontList = [];
|
|
684
|
+
|
|
685
|
+
async function loadFontList() {
|
|
686
|
+
if (_fontList.length > 0) return;
|
|
687
|
+
try {
|
|
688
|
+
const res = await fetch("/api/fonts");
|
|
689
|
+
_fontList = await res.json();
|
|
690
|
+
} catch { _fontList = []; }
|
|
691
|
+
populateFontSelects();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function populateFontSelects() {
|
|
695
|
+
for (const id of ["bk-font-heading", "bk-font-body"]) {
|
|
696
|
+
const sel = document.getElementById(id);
|
|
697
|
+
if (!sel || sel.options.length > 1) continue;
|
|
698
|
+
|
|
699
|
+
const categories = ["system", "sans-serif", "serif", "display", "monospace"];
|
|
700
|
+
const catLabels = { system: "System", "sans-serif": "Sans-Serif", serif: "Serif", display: "Display", monospace: "Monospace" };
|
|
701
|
+
for (const cat of categories) {
|
|
702
|
+
const group = document.createElement("optgroup");
|
|
703
|
+
group.label = catLabels[cat] || cat;
|
|
704
|
+
for (const f of _fontList.filter((x) => x.category === cat)) {
|
|
705
|
+
const opt = document.createElement("option");
|
|
706
|
+
opt.value = f.stack;
|
|
707
|
+
opt.textContent = f.name;
|
|
708
|
+
opt.style.fontFamily = f.stack;
|
|
709
|
+
group.appendChild(opt);
|
|
710
|
+
}
|
|
711
|
+
sel.appendChild(group);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function renderBrandKit(brandKit) {
|
|
717
|
+
const fields = {
|
|
718
|
+
primary: { color: "bk-color-primary", hex: "bk-hex-primary" },
|
|
719
|
+
secondary: { color: "bk-color-secondary", hex: "bk-hex-secondary" },
|
|
720
|
+
accent: { color: "bk-color-accent", hex: "bk-hex-accent" },
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
for (const [key, ids] of Object.entries(fields)) {
|
|
724
|
+
const colorInput = document.getElementById(ids.color);
|
|
725
|
+
const hexInput = document.getElementById(ids.hex);
|
|
726
|
+
const val = brandKit?.colors?.[key] || "";
|
|
727
|
+
if (colorInput) colorInput.value = val || colorInput.value;
|
|
728
|
+
if (hexInput) hexInput.value = val;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const headingSelect = document.getElementById("bk-font-heading");
|
|
732
|
+
const bodySelect = document.getElementById("bk-font-body");
|
|
733
|
+
const logoInput = document.getElementById("bk-logo-url");
|
|
734
|
+
|
|
735
|
+
if (headingSelect) selectFontValue(headingSelect, brandKit?.fonts?.heading || "");
|
|
736
|
+
if (bodySelect) selectFontValue(bodySelect, brandKit?.fonts?.body || "");
|
|
737
|
+
if (logoInput) logoInput.value = brandKit?.logoUrl || "";
|
|
738
|
+
|
|
739
|
+
updateBrandPreview();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function selectFontValue(selectEl, value) {
|
|
743
|
+
if (!value) { selectEl.value = ""; return; }
|
|
744
|
+
const lower = value.toLowerCase().trim();
|
|
745
|
+
for (const opt of selectEl.options) {
|
|
746
|
+
if (opt.value.toLowerCase().trim() === lower) {
|
|
747
|
+
selectEl.value = opt.value;
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const custom = document.createElement("option");
|
|
752
|
+
custom.value = value;
|
|
753
|
+
custom.textContent = value.split(",")[0].replace(/['"]/g, "").trim() + " (custom)";
|
|
754
|
+
const firstOptgroup = selectEl.querySelector("optgroup");
|
|
755
|
+
selectEl.insertBefore(custom, firstOptgroup || selectEl.options[1]);
|
|
756
|
+
selectEl.value = value;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function updateBrandPreview() {
|
|
760
|
+
const hexRe = /^#[0-9a-fA-F]{6}$/;
|
|
761
|
+
const swatchKeys = ["primary", "secondary", "accent"];
|
|
762
|
+
for (const key of swatchKeys) {
|
|
763
|
+
const swatch = document.getElementById(`brand-preview-swatch-${key}`);
|
|
764
|
+
const hex = document.getElementById(`bk-hex-${key}`)?.value?.trim();
|
|
765
|
+
if (!swatch) continue;
|
|
766
|
+
if (hex && hexRe.test(hex)) {
|
|
767
|
+
swatch.style.background = hex;
|
|
768
|
+
swatch.dataset.empty = "false";
|
|
769
|
+
swatch.title = `${key.charAt(0).toUpperCase() + key.slice(1)} ${hex}`;
|
|
770
|
+
} else {
|
|
771
|
+
swatch.style.background = "";
|
|
772
|
+
swatch.dataset.empty = "true";
|
|
773
|
+
swatch.title = `${key.charAt(0).toUpperCase() + key.slice(1)} (not set)`;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const headingFont = document.getElementById("bk-font-heading")?.value || "";
|
|
778
|
+
const bodyFont = document.getElementById("bk-font-body")?.value || "";
|
|
779
|
+
const headingPreview = document.getElementById("brand-preview-heading");
|
|
780
|
+
const bodyPreview = document.getElementById("brand-preview-body");
|
|
781
|
+
if (headingPreview) headingPreview.style.fontFamily = headingFont || "Georgia, serif";
|
|
782
|
+
if (bodyPreview) bodyPreview.style.fontFamily = bodyFont || "Arial, Helvetica, sans-serif";
|
|
783
|
+
|
|
784
|
+
const logoUrl = document.getElementById("bk-logo-url")?.value?.trim();
|
|
785
|
+
const logoImg = document.getElementById("brand-preview-logo");
|
|
786
|
+
const logoPlaceholder = document.getElementById("brand-preview-logo-placeholder");
|
|
787
|
+
if (logoImg && logoPlaceholder) {
|
|
788
|
+
if (logoUrl) {
|
|
789
|
+
logoImg.src = logoUrl;
|
|
790
|
+
logoImg.hidden = false;
|
|
791
|
+
logoPlaceholder.hidden = true;
|
|
792
|
+
logoImg.onerror = () => {
|
|
793
|
+
logoImg.hidden = true;
|
|
794
|
+
logoPlaceholder.hidden = false;
|
|
795
|
+
logoPlaceholder.textContent = "Bad URL";
|
|
796
|
+
};
|
|
797
|
+
logoImg.onload = () => {
|
|
798
|
+
logoPlaceholder.textContent = "No logo";
|
|
799
|
+
};
|
|
800
|
+
} else {
|
|
801
|
+
logoImg.hidden = true;
|
|
802
|
+
logoImg.removeAttribute("src");
|
|
803
|
+
logoPlaceholder.hidden = false;
|
|
804
|
+
logoPlaceholder.textContent = "No logo";
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
for (const id of [
|
|
810
|
+
"bk-hex-primary", "bk-hex-secondary", "bk-hex-accent",
|
|
811
|
+
"bk-color-primary", "bk-color-secondary", "bk-color-accent",
|
|
812
|
+
"bk-logo-url",
|
|
813
|
+
]) {
|
|
814
|
+
document.getElementById(id)?.addEventListener("input", updateBrandPreview);
|
|
815
|
+
}
|
|
816
|
+
for (const id of ["bk-font-heading", "bk-font-body"]) {
|
|
817
|
+
document.getElementById(id)?.addEventListener("change", updateBrandPreview);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
document.addEventListener("DOMContentLoaded", () => { loadFontList(); updateBrandPreview(); });
|
|
821
|
+
|
|
822
|
+
function collectBrandKit() {
|
|
823
|
+
const kit = {};
|
|
824
|
+
const primary = document.getElementById("bk-hex-primary")?.value?.trim();
|
|
825
|
+
const secondary = document.getElementById("bk-hex-secondary")?.value?.trim();
|
|
826
|
+
const accent = document.getElementById("bk-hex-accent")?.value?.trim();
|
|
827
|
+
const hexRe = /^#[0-9a-fA-F]{6}$/;
|
|
828
|
+
const colors = {};
|
|
829
|
+
if (primary && hexRe.test(primary)) colors.primary = primary;
|
|
830
|
+
if (secondary && hexRe.test(secondary)) colors.secondary = secondary;
|
|
831
|
+
if (accent && hexRe.test(accent)) colors.accent = accent;
|
|
832
|
+
if (Object.keys(colors).length > 0) kit.colors = colors;
|
|
833
|
+
|
|
834
|
+
const heading = document.getElementById("bk-font-heading")?.value || "";
|
|
835
|
+
const body = document.getElementById("bk-font-body")?.value || "";
|
|
836
|
+
const fonts = {};
|
|
837
|
+
if (heading) fonts.heading = heading;
|
|
838
|
+
if (body) fonts.body = body;
|
|
839
|
+
if (Object.keys(fonts).length > 0) kit.fonts = fonts;
|
|
840
|
+
|
|
841
|
+
const logo = document.getElementById("bk-logo-url")?.value?.trim();
|
|
842
|
+
if (logo) kit.logoUrl = logo;
|
|
843
|
+
|
|
844
|
+
return kit;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Sync color picker ↔ hex input
|
|
848
|
+
for (const key of ["primary", "secondary", "accent"]) {
|
|
849
|
+
const colorInput = document.getElementById(`bk-color-${key}`);
|
|
850
|
+
const hexInput = document.getElementById(`bk-hex-${key}`);
|
|
851
|
+
if (colorInput && hexInput) {
|
|
852
|
+
colorInput.addEventListener("input", () => { hexInput.value = colorInput.value; });
|
|
853
|
+
hexInput.addEventListener("input", () => {
|
|
854
|
+
if (/^#[0-9a-fA-F]{6}$/.test(hexInput.value)) colorInput.value = hexInput.value;
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
document.getElementById("bk-save")?.addEventListener("click", async () => {
|
|
860
|
+
const kit = collectBrandKit();
|
|
861
|
+
if (Object.keys(kit).length === 0) {
|
|
862
|
+
await vibeAlert("Please fill in at least one field.", "Info");
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const res = await fetch("/api/brand-kit", {
|
|
867
|
+
method: "POST",
|
|
868
|
+
headers: { "Content-Type": "application/json" },
|
|
869
|
+
body: JSON.stringify(kit),
|
|
870
|
+
});
|
|
871
|
+
const data = await res.json();
|
|
872
|
+
if (data.ok) {
|
|
873
|
+
await vibeAlert("Brand kit saved.", "Success");
|
|
874
|
+
} else {
|
|
875
|
+
await vibeAlert(data.error || "Failed to save brand kit.", "Error");
|
|
876
|
+
}
|
|
877
|
+
} catch (err) {
|
|
878
|
+
await vibeAlert("Failed to save: " + err.message, "Error");
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
document.getElementById("bk-clear")?.addEventListener("click", async () => {
|
|
883
|
+
const ok = await vibeConfirm("Clear brand kit?", "This will remove all brand kit settings.", { confirmLabel: "Clear", confirmClass: "btn--danger" });
|
|
884
|
+
if (!ok) return;
|
|
885
|
+
try {
|
|
886
|
+
await fetch("/api/brand-kit", { method: "DELETE" });
|
|
887
|
+
renderBrandKit(null);
|
|
888
|
+
} catch (err) {
|
|
889
|
+
await vibeAlert("Failed to clear: " + err.message, "Error");
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
|
|
419
893
|
// ---------------------------------------------------------------------------
|
|
420
894
|
// Actions
|
|
421
895
|
// ---------------------------------------------------------------------------
|
|
@@ -426,6 +900,7 @@ async function createTemplateFromPageType(pageType) {
|
|
|
426
900
|
blog_post: "Blog Post",
|
|
427
901
|
website_page: "Website Page",
|
|
428
902
|
module_only: "Module",
|
|
903
|
+
email: "Email",
|
|
429
904
|
};
|
|
430
905
|
|
|
431
906
|
const label = await vibePrompt("Template name", defaultLabels[pageType] || "New Template");
|
|
@@ -443,8 +918,12 @@ async function createTemplateFromPageType(pageType) {
|
|
|
443
918
|
return;
|
|
444
919
|
}
|
|
445
920
|
|
|
446
|
-
//
|
|
447
|
-
openTemplate(data.template.id);
|
|
921
|
+
// Navigate to the editor with the new asset selected
|
|
922
|
+
await openTemplate(data.template.id);
|
|
923
|
+
|
|
924
|
+
// Focus the chat input so the user can start describing their page
|
|
925
|
+
const chatInput = document.getElementById("chat-input");
|
|
926
|
+
if (chatInput) setTimeout(() => chatInput.focus(), 100);
|
|
448
927
|
} catch (err) {
|
|
449
928
|
await vibeAlert("Failed to create template: " + err.message, "Error");
|
|
450
929
|
}
|
|
@@ -561,6 +1040,10 @@ async function handleBrandFileSelected(type, file) {
|
|
|
561
1040
|
await vibeAlert(data.error, "Error");
|
|
562
1041
|
return;
|
|
563
1042
|
}
|
|
1043
|
+
if (data.brandKit) {
|
|
1044
|
+
await loadFontList();
|
|
1045
|
+
renderBrandKit(data.brandKit);
|
|
1046
|
+
}
|
|
564
1047
|
refreshDashboard();
|
|
565
1048
|
} catch (err) {
|
|
566
1049
|
await vibeAlert("Failed to upload: " + err.message, "Error");
|
|
@@ -572,22 +1055,19 @@ async function handleBrandFileSelected(type, file) {
|
|
|
572
1055
|
// ---------------------------------------------------------------------------
|
|
573
1056
|
|
|
574
1057
|
function showChat(themeName, templateId) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
appScreen.classList.remove("hidden");
|
|
1058
|
+
const ab = document.getElementById("app-body");
|
|
1059
|
+
if (ab) ab.dataset.mode = "editor";
|
|
1060
|
+
dashboardScreen.classList.remove("hidden");
|
|
579
1061
|
document.getElementById("theme-name").textContent = themeName;
|
|
580
1062
|
|
|
581
|
-
// Update browser chrome URL bar
|
|
582
|
-
const urlEl = document.getElementById("browser-url");
|
|
583
|
-
if (urlEl) urlEl.textContent = themeName + ".vibespot.app";
|
|
584
|
-
|
|
585
1063
|
// Update URL
|
|
586
1064
|
const target = `#/app/${encodeURIComponent(themeName)}/${encodeURIComponent(templateId)}`;
|
|
587
1065
|
if (location.hash !== target) {
|
|
588
1066
|
history.pushState(null, "", target);
|
|
589
1067
|
}
|
|
590
1068
|
|
|
1069
|
+
switchWorkspaceTab("pages");
|
|
1070
|
+
|
|
591
1071
|
// Connect WebSocket (defined in chat.js)
|
|
592
1072
|
if (typeof connectWebSocket === "function") {
|
|
593
1073
|
connectWebSocket();
|
|
@@ -597,6 +1077,8 @@ function showChat(themeName, templateId) {
|
|
|
597
1077
|
if (typeof refreshPreview === "function") {
|
|
598
1078
|
refreshPreview();
|
|
599
1079
|
}
|
|
1080
|
+
|
|
1081
|
+
setTimeout(() => document.getElementById("chat-input")?.focus(), 100);
|
|
600
1082
|
}
|
|
601
1083
|
|
|
602
1084
|
// ---------------------------------------------------------------------------
|
|
@@ -610,26 +1092,11 @@ document.querySelectorAll(".page-type-card").forEach((card) => {
|
|
|
610
1092
|
});
|
|
611
1093
|
});
|
|
612
1094
|
|
|
613
|
-
//
|
|
614
|
-
document.getElementById("dashboard-back").addEventListener("click", () => {
|
|
615
|
-
hideDashboard();
|
|
616
|
-
if (typeof showSetup === "function") showSetup();
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
// Settings button
|
|
620
|
-
document.getElementById("dashboard-settings-btn").addEventListener("click", () => {
|
|
621
|
-
if (typeof openSettings === "function") openSettings();
|
|
622
|
-
});
|
|
1095
|
+
// Deploy button — handled by chat.js (single listener to avoid duplicate overlays)
|
|
623
1096
|
|
|
624
|
-
//
|
|
625
|
-
document.getElementById("
|
|
626
|
-
|
|
627
|
-
// Need to show app screen temporarily for upload
|
|
628
|
-
appScreen.classList.remove("hidden");
|
|
629
|
-
dashboardScreen.classList.add("hidden");
|
|
630
|
-
startUpload();
|
|
631
|
-
}
|
|
632
|
-
});
|
|
1097
|
+
// Library tab — add page / add email
|
|
1098
|
+
document.getElementById("library-add-page")?.addEventListener("click", () => createTemplateFromPageType("landing_page"));
|
|
1099
|
+
document.getElementById("library-add-email")?.addEventListener("click", () => createTemplateFromPageType("email"));
|
|
633
1100
|
|
|
634
1101
|
// Extract All button
|
|
635
1102
|
document.getElementById("btn-extract-all")?.addEventListener("click", async () => {
|
|
@@ -658,13 +1125,18 @@ document.getElementById("btn-extract-all")?.addEventListener("click", async () =
|
|
|
658
1125
|
});
|
|
659
1126
|
const data = await res.json();
|
|
660
1127
|
if (data.ok) {
|
|
1128
|
+
if (data.brandKit) {
|
|
1129
|
+
await loadFontList();
|
|
1130
|
+
renderBrandKit(data.brandKit);
|
|
1131
|
+
}
|
|
661
1132
|
await refreshDashboard();
|
|
662
1133
|
const extracted = data.extracted || {};
|
|
663
1134
|
const names = Object.entries(extracted)
|
|
664
1135
|
.filter(([, v]) => v)
|
|
665
1136
|
.map(([k]) => ASSET_LABELS[k] || k);
|
|
666
1137
|
if (names.length > 0) {
|
|
667
|
-
|
|
1138
|
+
const suffix = data.brandKit ? " Brand kit updated from styleguide." : "";
|
|
1139
|
+
await vibeAlert(`Extracted: ${names.join(", ")}.${suffix}`, "Done");
|
|
668
1140
|
} else {
|
|
669
1141
|
await vibeAlert("Nothing to extract \u2014 generate some modules first.", "Info");
|
|
670
1142
|
}
|
|
@@ -728,9 +1200,9 @@ document.getElementById("btn-import-reference")?.addEventListener("click", async
|
|
|
728
1200
|
}
|
|
729
1201
|
});
|
|
730
1202
|
|
|
731
|
-
//
|
|
732
|
-
document.getElementById("
|
|
733
|
-
const el = document.getElementById("
|
|
1203
|
+
// Theme name pill — double-click to rename
|
|
1204
|
+
document.getElementById("theme-name")?.addEventListener("dblclick", () => {
|
|
1205
|
+
const el = document.getElementById("theme-name");
|
|
734
1206
|
if (!el || !currentDashboardSessionId) return;
|
|
735
1207
|
if (el.contentEditable === "true") return;
|
|
736
1208
|
|
|
@@ -765,7 +1237,7 @@ document.getElementById("dashboard-theme-heading")?.addEventListener("dblclick",
|
|
|
765
1237
|
if (data.ok) {
|
|
766
1238
|
el.textContent = data.newName;
|
|
767
1239
|
currentDashboardTheme = data.newName;
|
|
768
|
-
document.getElementById("
|
|
1240
|
+
document.getElementById("theme-name").textContent = data.newName;
|
|
769
1241
|
window.location.hash = "#/dashboard/" + encodeURIComponent(data.newName);
|
|
770
1242
|
// Update rail
|
|
771
1243
|
const railItem = document.querySelector(`.project-rail__item[data-name="${oldName}"]`);
|
|
@@ -803,36 +1275,6 @@ document.getElementById("dashboard-theme-heading")?.addEventListener("dblclick",
|
|
|
803
1275
|
});
|
|
804
1276
|
});
|
|
805
1277
|
|
|
806
|
-
// Download ZIP button
|
|
807
|
-
document.getElementById("dashboard-download-zip").addEventListener("click", async () => {
|
|
808
|
-
const btn = document.getElementById("dashboard-download-zip");
|
|
809
|
-
const origHTML = btn.innerHTML;
|
|
810
|
-
btn.disabled = true;
|
|
811
|
-
btn.querySelector("span").textContent = "Downloading...";
|
|
812
|
-
|
|
813
|
-
try {
|
|
814
|
-
const res = await fetch("/api/download-zip");
|
|
815
|
-
if (!res.ok) {
|
|
816
|
-
const err = await res.json().catch(() => ({ error: "Download failed" }));
|
|
817
|
-
throw new Error(err.error || "Download failed");
|
|
818
|
-
}
|
|
819
|
-
const blob = await res.blob();
|
|
820
|
-
const url = URL.createObjectURL(blob);
|
|
821
|
-
const a = document.createElement("a");
|
|
822
|
-
a.href = url;
|
|
823
|
-
a.download = (currentDashboardTheme || "theme") + ".zip";
|
|
824
|
-
document.body.appendChild(a);
|
|
825
|
-
a.click();
|
|
826
|
-
a.remove();
|
|
827
|
-
URL.revokeObjectURL(url);
|
|
828
|
-
} catch (err) {
|
|
829
|
-
if (typeof vibeAlert === "function") vibeAlert(err.message, "Error");
|
|
830
|
-
} finally {
|
|
831
|
-
btn.disabled = false;
|
|
832
|
-
btn.innerHTML = origHTML;
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
|
|
836
1278
|
// Humanify toggle
|
|
837
1279
|
const humanifyCheckbox = document.getElementById("humanify-checkbox");
|
|
838
1280
|
if (humanifyCheckbox) {
|
|
@@ -844,3 +1286,29 @@ if (humanifyCheckbox) {
|
|
|
844
1286
|
});
|
|
845
1287
|
});
|
|
846
1288
|
}
|
|
1289
|
+
|
|
1290
|
+
// ---------------------------------------------------------------------------
|
|
1291
|
+
// Workspace tab navigation
|
|
1292
|
+
// ---------------------------------------------------------------------------
|
|
1293
|
+
|
|
1294
|
+
function switchWorkspaceTab(tabName) {
|
|
1295
|
+
document.querySelectorAll(".workspace-tab").forEach((btn) => {
|
|
1296
|
+
btn.classList.toggle("active", btn.dataset.wsTab === tabName);
|
|
1297
|
+
});
|
|
1298
|
+
document.querySelectorAll(".workspace-panel").forEach((panel) => {
|
|
1299
|
+
panel.classList.toggle("active", panel.dataset.wsPanel === tabName);
|
|
1300
|
+
});
|
|
1301
|
+
if (tabName === "settings" && typeof refreshSettings === "function") {
|
|
1302
|
+
refreshSettings();
|
|
1303
|
+
}
|
|
1304
|
+
if (tabName === "library") {
|
|
1305
|
+
refreshDashboard();
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
document.querySelectorAll(".workspace-tab").forEach((btn) => {
|
|
1310
|
+
btn.addEventListener("click", () => {
|
|
1311
|
+
switchWorkspaceTab(btn.dataset.wsTab);
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
|