vibespot 1.2.0 → 1.3.1
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 +54 -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 +416 -119
- package/ui/docs/screenshots/asset-type-cards.png +0 -0
- package/ui/docs/screenshots/brand-kit-preview.png +0 -0
- package/ui/docs/screenshots/content-type-dropdown.png +0 -0
- package/ui/docs/screenshots/deploy-progress.png +0 -0
- package/ui/docs/screenshots/editor-full-layout.png +0 -0
- package/ui/docs/screenshots/email-client-preview.png +0 -0
- package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
- package/ui/docs/screenshots/module-overview-slideout.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/pipeline-progress.png +0 -0
- package/ui/docs/screenshots/project-overview-table.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
package/ui/dashboard.js
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
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 = {
|
|
10
10
|
landing_page: "LP",
|
|
11
11
|
blog_post: "Blog",
|
|
12
12
|
website_page: "Web",
|
|
13
|
-
module_only: "
|
|
13
|
+
module_only: "Mod",
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
const PAGE_TYPE_FULL_LABELS = {
|
|
17
17
|
landing_page: "Landing Page",
|
|
18
18
|
blog_post: "Blog Post",
|
|
19
19
|
website_page: "Website Page",
|
|
20
|
-
module_only: "
|
|
20
|
+
module_only: "Module Only",
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
@@ -31,16 +31,13 @@ let currentDashboardIsImported = false;
|
|
|
31
31
|
async function showDashboard(themeName) {
|
|
32
32
|
currentDashboardTheme = themeName;
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
document.getElementById("
|
|
37
|
-
document.getElementById("project-rail")?.classList.remove("project-rail--expanded");
|
|
38
|
-
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");
|
|
39
37
|
dashboardScreen.classList.remove("hidden");
|
|
40
38
|
|
|
41
|
-
document.getElementById("
|
|
42
|
-
|
|
43
|
-
document.getElementById("dashboard-theme-path-text").textContent = "";
|
|
39
|
+
document.getElementById("theme-name").textContent = themeName;
|
|
40
|
+
if (typeof updateRailActive === "function") updateRailActive();
|
|
44
41
|
|
|
45
42
|
// Get sessionId for the active theme
|
|
46
43
|
try {
|
|
@@ -61,10 +58,16 @@ async function showDashboard(themeName) {
|
|
|
61
58
|
|
|
62
59
|
// Load dashboard data
|
|
63
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
|
+
}
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
function hideDashboard() {
|
|
67
|
-
dashboardScreen.classList.add("hidden");
|
|
68
71
|
currentDashboardTheme = "";
|
|
69
72
|
currentDashboardIsImported = false;
|
|
70
73
|
closeModulePreview();
|
|
@@ -83,10 +86,14 @@ async function refreshDashboard() {
|
|
|
83
86
|
return;
|
|
84
87
|
}
|
|
85
88
|
renderTemplateList(data.templates || []);
|
|
89
|
+
renderProjectAssets(data.templates || []);
|
|
86
90
|
renderModuleLibrary(data.moduleLibrary || []);
|
|
87
91
|
renderBrandAssets(data.brandAssets || {});
|
|
88
|
-
|
|
89
|
-
|
|
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;
|
|
90
97
|
}
|
|
91
98
|
if (currentDashboardIsImported) {
|
|
92
99
|
await refreshInverseAnalysis();
|
|
@@ -276,7 +283,8 @@ document.getElementById("btn-inverse-apply-tokens")?.addEventListener("click", a
|
|
|
276
283
|
function renderTemplateList(templates) {
|
|
277
284
|
const list = document.getElementById("dashboard-template-list");
|
|
278
285
|
const countEl = document.getElementById("dashboard-template-count");
|
|
279
|
-
|
|
286
|
+
if (!list) return;
|
|
287
|
+
if (countEl) countEl.textContent = templates.length;
|
|
280
288
|
|
|
281
289
|
if (templates.length === 0) {
|
|
282
290
|
list.innerHTML = `<p class="dashboard__empty-state">No templates yet. Choose a page type above to get started.</p>`;
|
|
@@ -290,9 +298,9 @@ function renderTemplateList(templates) {
|
|
|
290
298
|
item.innerHTML = `
|
|
291
299
|
<span class="dashboard__template-badge dashboard__template-badge--${tpl.pageType}">${esc(PAGE_TYPE_LABELS[tpl.pageType] || "?")}</span>
|
|
292
300
|
<span class="dashboard__template-label">${esc(tpl.label)}</span>
|
|
293
|
-
<span class="dashboard__template-meta">${tpl.moduleCount}
|
|
301
|
+
<span class="dashboard__template-meta">${tpl.moduleCount} module${tpl.moduleCount !== 1 ? "s" : ""}</span>
|
|
294
302
|
<button class="btn btn--sm btn--primary dashboard__template-open" data-id="${esc(tpl.id)}">Open</button>
|
|
295
|
-
<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>
|
|
296
304
|
<button class="dashboard__template-delete" data-id="${esc(tpl.id)}" title="Delete template">×</button>
|
|
297
305
|
`;
|
|
298
306
|
list.appendChild(item);
|
|
@@ -375,6 +383,70 @@ function startTemplateRename(labelEl, templateId) {
|
|
|
375
383
|
});
|
|
376
384
|
}
|
|
377
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
|
+
|
|
378
450
|
// ---------------------------------------------------------------------------
|
|
379
451
|
// Module library
|
|
380
452
|
// ---------------------------------------------------------------------------
|
|
@@ -383,9 +455,10 @@ let activePreviewModule = "";
|
|
|
383
455
|
|
|
384
456
|
function renderModuleLibrary(modules) {
|
|
385
457
|
const container = document.getElementById("dashboard-module-library");
|
|
458
|
+
if (!container) return;
|
|
386
459
|
|
|
387
460
|
if (modules.length === 0) {
|
|
388
|
-
container.innerHTML = `<p class="dashboard__empty-state">
|
|
461
|
+
container.innerHTML = `<p class="dashboard__empty-state">Modules will appear here as you build pages.</p>`;
|
|
389
462
|
closeModulePreview();
|
|
390
463
|
return;
|
|
391
464
|
}
|
|
@@ -442,22 +515,22 @@ async function showModulePreview(moduleName, usedIn) {
|
|
|
442
515
|
function closeModulePreview() {
|
|
443
516
|
activePreviewModule = "";
|
|
444
517
|
const previewEl = document.getElementById("dashboard-module-preview");
|
|
445
|
-
previewEl.classList.add("hidden");
|
|
518
|
+
if (previewEl) previewEl.classList.add("hidden");
|
|
446
519
|
document.querySelectorAll(".dashboard__module-chip").forEach((c) => {
|
|
447
520
|
c.classList.remove("dashboard__module-chip--active");
|
|
448
521
|
});
|
|
449
522
|
}
|
|
450
523
|
|
|
451
524
|
// Close button for module preview
|
|
452
|
-
document.getElementById("dashboard-preview-close")
|
|
525
|
+
document.getElementById("dashboard-preview-close")?.addEventListener("click", closeModulePreview);
|
|
453
526
|
|
|
454
527
|
// Delete button for module preview
|
|
455
|
-
document.getElementById("dashboard-preview-delete")
|
|
528
|
+
document.getElementById("dashboard-preview-delete")?.addEventListener("click", async () => {
|
|
456
529
|
const moduleName = activePreviewModule;
|
|
457
530
|
if (!moduleName) return;
|
|
458
531
|
|
|
459
532
|
const ok = await vibeConfirm(
|
|
460
|
-
`Delete
|
|
533
|
+
`Delete module "${moduleName}"?`,
|
|
461
534
|
"This will remove it from all templates and delete it from disk.",
|
|
462
535
|
{ confirmLabel: "Delete" }
|
|
463
536
|
);
|
|
@@ -472,7 +545,7 @@ document.getElementById("dashboard-preview-delete").addEventListener("click", as
|
|
|
472
545
|
closeModulePreview();
|
|
473
546
|
await refreshDashboard();
|
|
474
547
|
} catch (err) {
|
|
475
|
-
await vibeAlert("Failed to delete
|
|
548
|
+
await vibeAlert("Failed to delete module: " + err.message, "Error");
|
|
476
549
|
}
|
|
477
550
|
});
|
|
478
551
|
|
|
@@ -559,15 +632,20 @@ async function extractBrandAsset(type, card) {
|
|
|
559
632
|
});
|
|
560
633
|
const data = await res.json();
|
|
561
634
|
if (data.ok && data.content) {
|
|
635
|
+
if (data.brandKit) {
|
|
636
|
+
await loadFontList();
|
|
637
|
+
renderBrandKit(data.brandKit);
|
|
638
|
+
}
|
|
562
639
|
await refreshDashboard();
|
|
640
|
+
const msg = data.brandKit ? `${ASSET_LABELS[type]} extracted. Brand kit updated from styleguide.` : `${ASSET_LABELS[type]} extracted.`;
|
|
563
641
|
const view = await vibeConfirm(
|
|
564
|
-
|
|
642
|
+
msg,
|
|
565
643
|
"Would you like to view it?",
|
|
566
644
|
{ confirmLabel: "View", confirmClass: "btn--primary" },
|
|
567
645
|
);
|
|
568
646
|
if (view) await vibeViewContent(data.content, ASSET_LABELS[type], ASSET_FILES[type]);
|
|
569
647
|
} else {
|
|
570
|
-
await vibeAlert(data.error || "Nothing to extract — generate some
|
|
648
|
+
await vibeAlert(data.error || "Nothing to extract — generate some modules first.", "Info");
|
|
571
649
|
}
|
|
572
650
|
} catch (err) {
|
|
573
651
|
await vibeAlert("Extraction failed: " + err.message, "Error");
|
|
@@ -598,6 +676,220 @@ document.getElementById("dashboard-brand-assets")?.addEventListener("change", (e
|
|
|
598
676
|
handleBrandFileSelected(card.dataset.asset, e.target.files[0]);
|
|
599
677
|
});
|
|
600
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
|
+
|
|
601
893
|
// ---------------------------------------------------------------------------
|
|
602
894
|
// Actions
|
|
603
895
|
// ---------------------------------------------------------------------------
|
|
@@ -607,7 +899,8 @@ async function createTemplateFromPageType(pageType) {
|
|
|
607
899
|
landing_page: "Landing Page",
|
|
608
900
|
blog_post: "Blog Post",
|
|
609
901
|
website_page: "Website Page",
|
|
610
|
-
module_only: "
|
|
902
|
+
module_only: "Module",
|
|
903
|
+
email: "Email",
|
|
611
904
|
};
|
|
612
905
|
|
|
613
906
|
const label = await vibePrompt("Template name", defaultLabels[pageType] || "New Template");
|
|
@@ -625,8 +918,12 @@ async function createTemplateFromPageType(pageType) {
|
|
|
625
918
|
return;
|
|
626
919
|
}
|
|
627
920
|
|
|
628
|
-
//
|
|
629
|
-
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);
|
|
630
927
|
} catch (err) {
|
|
631
928
|
await vibeAlert("Failed to create template: " + err.message, "Error");
|
|
632
929
|
}
|
|
@@ -684,8 +981,8 @@ function vibeDeleteTemplateDialog() {
|
|
|
684
981
|
<div class="confirm-dialog__title">Delete template?</div>
|
|
685
982
|
<p class="confirm-dialog__warn">This cannot be undone.</p>
|
|
686
983
|
<div class="confirm-dialog__actions" style="flex-direction:column;gap:8px">
|
|
687
|
-
<button class="btn btn--danger" data-action="with_modules" style="width:100%">Delete template and its
|
|
688
|
-
<button class="btn btn--secondary" data-action="template_only" style="width:100%">Delete template only (keep
|
|
984
|
+
<button class="btn btn--danger" data-action="with_modules" style="width:100%">Delete template and its modules</button>
|
|
985
|
+
<button class="btn btn--secondary" data-action="template_only" style="width:100%">Delete template only (keep modules)</button>
|
|
689
986
|
<button class="btn btn--secondary" data-action="cancel" style="width:100%">Cancel</button>
|
|
690
987
|
</div>
|
|
691
988
|
</div>
|
|
@@ -743,6 +1040,10 @@ async function handleBrandFileSelected(type, file) {
|
|
|
743
1040
|
await vibeAlert(data.error, "Error");
|
|
744
1041
|
return;
|
|
745
1042
|
}
|
|
1043
|
+
if (data.brandKit) {
|
|
1044
|
+
await loadFontList();
|
|
1045
|
+
renderBrandKit(data.brandKit);
|
|
1046
|
+
}
|
|
746
1047
|
refreshDashboard();
|
|
747
1048
|
} catch (err) {
|
|
748
1049
|
await vibeAlert("Failed to upload: " + err.message, "Error");
|
|
@@ -754,22 +1055,19 @@ async function handleBrandFileSelected(type, file) {
|
|
|
754
1055
|
// ---------------------------------------------------------------------------
|
|
755
1056
|
|
|
756
1057
|
function showChat(themeName, templateId) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
appScreen.classList.remove("hidden");
|
|
1058
|
+
const ab = document.getElementById("app-body");
|
|
1059
|
+
if (ab) ab.dataset.mode = "editor";
|
|
1060
|
+
dashboardScreen.classList.remove("hidden");
|
|
761
1061
|
document.getElementById("theme-name").textContent = themeName;
|
|
762
1062
|
|
|
763
|
-
// Update browser chrome URL bar
|
|
764
|
-
const urlEl = document.getElementById("browser-url");
|
|
765
|
-
if (urlEl) urlEl.textContent = themeName + ".vibespot.app";
|
|
766
|
-
|
|
767
1063
|
// Update URL
|
|
768
1064
|
const target = `#/app/${encodeURIComponent(themeName)}/${encodeURIComponent(templateId)}`;
|
|
769
1065
|
if (location.hash !== target) {
|
|
770
1066
|
history.pushState(null, "", target);
|
|
771
1067
|
}
|
|
772
1068
|
|
|
1069
|
+
switchWorkspaceTab("pages");
|
|
1070
|
+
|
|
773
1071
|
// Connect WebSocket (defined in chat.js)
|
|
774
1072
|
if (typeof connectWebSocket === "function") {
|
|
775
1073
|
connectWebSocket();
|
|
@@ -779,6 +1077,8 @@ function showChat(themeName, templateId) {
|
|
|
779
1077
|
if (typeof refreshPreview === "function") {
|
|
780
1078
|
refreshPreview();
|
|
781
1079
|
}
|
|
1080
|
+
|
|
1081
|
+
setTimeout(() => document.getElementById("chat-input")?.focus(), 100);
|
|
782
1082
|
}
|
|
783
1083
|
|
|
784
1084
|
// ---------------------------------------------------------------------------
|
|
@@ -792,26 +1092,11 @@ document.querySelectorAll(".page-type-card").forEach((card) => {
|
|
|
792
1092
|
});
|
|
793
1093
|
});
|
|
794
1094
|
|
|
795
|
-
//
|
|
796
|
-
document.getElementById("dashboard-back").addEventListener("click", () => {
|
|
797
|
-
hideDashboard();
|
|
798
|
-
if (typeof showSetup === "function") showSetup();
|
|
799
|
-
});
|
|
1095
|
+
// Deploy button — handled by chat.js (single listener to avoid duplicate overlays)
|
|
800
1096
|
|
|
801
|
-
//
|
|
802
|
-
document.getElementById("
|
|
803
|
-
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
// Deploy button
|
|
807
|
-
document.getElementById("dashboard-deploy-btn").addEventListener("click", () => {
|
|
808
|
-
if (typeof startUpload === "function") {
|
|
809
|
-
// Need to show app screen temporarily for upload
|
|
810
|
-
appScreen.classList.remove("hidden");
|
|
811
|
-
dashboardScreen.classList.add("hidden");
|
|
812
|
-
startUpload();
|
|
813
|
-
}
|
|
814
|
-
});
|
|
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"));
|
|
815
1100
|
|
|
816
1101
|
// Extract All button
|
|
817
1102
|
document.getElementById("btn-extract-all")?.addEventListener("click", async () => {
|
|
@@ -840,15 +1125,20 @@ document.getElementById("btn-extract-all")?.addEventListener("click", async () =
|
|
|
840
1125
|
});
|
|
841
1126
|
const data = await res.json();
|
|
842
1127
|
if (data.ok) {
|
|
1128
|
+
if (data.brandKit) {
|
|
1129
|
+
await loadFontList();
|
|
1130
|
+
renderBrandKit(data.brandKit);
|
|
1131
|
+
}
|
|
843
1132
|
await refreshDashboard();
|
|
844
1133
|
const extracted = data.extracted || {};
|
|
845
1134
|
const names = Object.entries(extracted)
|
|
846
1135
|
.filter(([, v]) => v)
|
|
847
1136
|
.map(([k]) => ASSET_LABELS[k] || k);
|
|
848
1137
|
if (names.length > 0) {
|
|
849
|
-
|
|
1138
|
+
const suffix = data.brandKit ? " Brand kit updated from styleguide." : "";
|
|
1139
|
+
await vibeAlert(`Extracted: ${names.join(", ")}.${suffix}`, "Done");
|
|
850
1140
|
} else {
|
|
851
|
-
await vibeAlert("Nothing to extract \u2014 generate some
|
|
1141
|
+
await vibeAlert("Nothing to extract \u2014 generate some modules first.", "Info");
|
|
852
1142
|
}
|
|
853
1143
|
} else {
|
|
854
1144
|
await vibeAlert(data.error || "Extraction failed", "Error");
|
|
@@ -910,9 +1200,9 @@ document.getElementById("btn-import-reference")?.addEventListener("click", async
|
|
|
910
1200
|
}
|
|
911
1201
|
});
|
|
912
1202
|
|
|
913
|
-
//
|
|
914
|
-
document.getElementById("
|
|
915
|
-
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");
|
|
916
1206
|
if (!el || !currentDashboardSessionId) return;
|
|
917
1207
|
if (el.contentEditable === "true") return;
|
|
918
1208
|
|
|
@@ -947,7 +1237,7 @@ document.getElementById("dashboard-theme-heading")?.addEventListener("dblclick",
|
|
|
947
1237
|
if (data.ok) {
|
|
948
1238
|
el.textContent = data.newName;
|
|
949
1239
|
currentDashboardTheme = data.newName;
|
|
950
|
-
document.getElementById("
|
|
1240
|
+
document.getElementById("theme-name").textContent = data.newName;
|
|
951
1241
|
window.location.hash = "#/dashboard/" + encodeURIComponent(data.newName);
|
|
952
1242
|
// Update rail
|
|
953
1243
|
const railItem = document.querySelector(`.project-rail__item[data-name="${oldName}"]`);
|
|
@@ -985,36 +1275,6 @@ document.getElementById("dashboard-theme-heading")?.addEventListener("dblclick",
|
|
|
985
1275
|
});
|
|
986
1276
|
});
|
|
987
1277
|
|
|
988
|
-
// Download ZIP button
|
|
989
|
-
document.getElementById("dashboard-download-zip").addEventListener("click", async () => {
|
|
990
|
-
const btn = document.getElementById("dashboard-download-zip");
|
|
991
|
-
const origHTML = btn.innerHTML;
|
|
992
|
-
btn.disabled = true;
|
|
993
|
-
btn.querySelector("span").textContent = "Downloading...";
|
|
994
|
-
|
|
995
|
-
try {
|
|
996
|
-
const res = await fetch("/api/download-zip");
|
|
997
|
-
if (!res.ok) {
|
|
998
|
-
const err = await res.json().catch(() => ({ error: "Download failed" }));
|
|
999
|
-
throw new Error(err.error || "Download failed");
|
|
1000
|
-
}
|
|
1001
|
-
const blob = await res.blob();
|
|
1002
|
-
const url = URL.createObjectURL(blob);
|
|
1003
|
-
const a = document.createElement("a");
|
|
1004
|
-
a.href = url;
|
|
1005
|
-
a.download = (currentDashboardTheme || "theme") + ".zip";
|
|
1006
|
-
document.body.appendChild(a);
|
|
1007
|
-
a.click();
|
|
1008
|
-
a.remove();
|
|
1009
|
-
URL.revokeObjectURL(url);
|
|
1010
|
-
} catch (err) {
|
|
1011
|
-
if (typeof vibeAlert === "function") vibeAlert(err.message, "Error");
|
|
1012
|
-
} finally {
|
|
1013
|
-
btn.disabled = false;
|
|
1014
|
-
btn.innerHTML = origHTML;
|
|
1015
|
-
}
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
1278
|
// Humanify toggle
|
|
1019
1279
|
const humanifyCheckbox = document.getElementById("humanify-checkbox");
|
|
1020
1280
|
if (humanifyCheckbox) {
|
|
@@ -1026,3 +1286,29 @@ if (humanifyCheckbox) {
|
|
|
1026
1286
|
});
|
|
1027
1287
|
});
|
|
1028
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
|
+
|