vibespot 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +44 -5
  2. package/assets/blog-rules.md +251 -0
  3. package/assets/email-rules.md +390 -0
  4. package/assets/humanify-guide.md +300 -101
  5. package/assets/plan-templates/blog-content-hub.md +18 -9
  6. package/assets/plan-templates/email-announcement.md +41 -0
  7. package/assets/plan-templates/email-event-invite.md +43 -0
  8. package/assets/plan-templates/email-newsletter.md +41 -0
  9. package/assets/plan-templates/email-re-engagement.md +42 -0
  10. package/assets/plan-templates/email-welcome.md +41 -0
  11. package/dist/index.js +1460 -387
  12. package/dist/index.js.map +1 -1
  13. package/package.json +5 -5
  14. package/starters/06-blog-content-hub.json +75 -0
  15. package/starters/06-email-welcome.json +60 -0
  16. package/starters/07-email-announcement.json +60 -0
  17. package/starters/08-email-newsletter.json +52 -0
  18. package/ui/chat.js +777 -63
  19. package/ui/code-editor.js +49 -7
  20. package/ui/dashboard.js +379 -93
  21. package/ui/docs/docs.css +29 -0
  22. package/ui/docs/index.html +186 -108
  23. package/ui/docs/screenshots/brand-kit-preview.png +0 -0
  24. package/ui/docs/screenshots/content-type-dropdown.png +0 -0
  25. package/ui/docs/screenshots/editor-full-layout.png +0 -0
  26. package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
  27. package/ui/docs/screenshots/multi-page-tree.png +0 -0
  28. package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
  29. package/ui/docs/screenshots/split-pane-view.png +0 -0
  30. package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
  31. package/ui/docs/screenshots/workspace-tabs.png +0 -0
  32. package/ui/email-preview.js +109 -0
  33. package/ui/field-editor.js +72 -1
  34. package/ui/icons.js +120 -0
  35. package/ui/index.html +877 -629
  36. package/ui/inline-edit.js +710 -0
  37. package/ui/plan.js +0 -0
  38. package/ui/preview.js +101 -198
  39. package/ui/section-controls.js +628 -0
  40. package/ui/settings.js +58 -16
  41. package/ui/setup.js +750 -140
  42. package/ui/styles.css +3430 -952
  43. 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("dashboard-screen");
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: "Sec",
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: "Section 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
- // Hide other screens
35
- setupScreen.classList.add("hidden");
36
- document.getElementById("setup-topbar").classList.add("hidden");
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("dashboard-theme-name").textContent = themeName;
42
- document.getElementById("dashboard-theme-heading").textContent = themeName;
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
- if (data.themePath) {
89
- document.getElementById("dashboard-theme-path-text").textContent = data.themePath;
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
- countEl.textContent = templates.length;
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} section${tpl.moduleCount !== 1 ? "s" : ""}</span>
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">&#x29C9;</button>
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">&times;</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">Sections will appear here as you build pages.</p>`;
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").addEventListener("click", closeModulePreview);
525
+ document.getElementById("dashboard-preview-close")?.addEventListener("click", closeModulePreview);
453
526
 
454
527
  // Delete button for module preview
455
- document.getElementById("dashboard-preview-delete").addEventListener("click", async () => {
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 section "${moduleName}"?`,
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 section: " + err.message, "Error");
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
- `${ASSET_LABELS[type]} extracted.`,
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 sections first.", "Info");
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: "Section",
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
- // Open the newly created template in chat
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 sections</button>
688
- <button class="btn btn--secondary" data-action="template_only" style="width:100%">Delete template only (keep sections)</button>
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
- hideDashboard();
758
-
759
- // Show app screen
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
- // Back button setup
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
- // Settings button
802
- document.getElementById("dashboard-settings-btn").addEventListener("click", () => {
803
- if (typeof openSettings === "function") openSettings();
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
- await vibeAlert(`Extracted: ${names.join(", ")}`, "Done");
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 sections first.", "Info");
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
- // Dashboard theme heading — double-click to rename
914
- document.getElementById("dashboard-theme-heading")?.addEventListener("dblclick", () => {
915
- const el = document.getElementById("dashboard-theme-heading");
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("dashboard-theme-name").textContent = data.newName;
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
+