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/setup.js CHANGED
@@ -1,25 +1,106 @@
1
- /* Theme init — runs synchronously before DOM to prevent flash */
1
+ /* Theme init — runs synchronously before DOM to prevent flash.
2
+ Light is the default to match HubSpot's light-first ecosystem. */
3
+ const VIBESPOT_THEMES = ["dark", "light", "hubspot"];
4
+ const VIBESPOT_THEME_LABELS = {
5
+ dark: "Dark",
6
+ light: "Light",
7
+ hubspot: "HubSpot Light",
8
+ };
2
9
  (function initTheme() {
3
10
  const stored = localStorage.getItem("vibespot-theme");
4
- const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
5
- const theme = stored || (prefersDark ? "dark" : "light");
11
+ const theme = VIBESPOT_THEMES.includes(stored) ? stored : "light";
6
12
  document.documentElement.setAttribute("data-theme", theme);
7
13
  })();
8
14
 
15
+ function syncThemeToggleLabel() {
16
+ const btn = document.querySelector(".theme-toggle");
17
+ if (!btn) return;
18
+ const current = document.documentElement.getAttribute("data-theme") || "dark";
19
+ const idx = VIBESPOT_THEMES.indexOf(current);
20
+ const next = VIBESPOT_THEMES[(idx + 1) % VIBESPOT_THEMES.length];
21
+ const label = `Theme: ${VIBESPOT_THEME_LABELS[current] || current} — switch to ${VIBESPOT_THEME_LABELS[next] || next}`;
22
+ btn.setAttribute("title", label);
23
+ btn.setAttribute("aria-label", label);
24
+ }
25
+ document.addEventListener("DOMContentLoaded", syncThemeToggleLabel);
26
+
9
27
  function toggleTheme() {
10
28
  const current = document.documentElement.getAttribute("data-theme") || "dark";
11
- const next = current === "dark" ? "light" : "dark";
29
+ const idx = VIBESPOT_THEMES.indexOf(current);
30
+ const next = VIBESPOT_THEMES[((idx === -1 ? 0 : idx) + 1) % VIBESPOT_THEMES.length];
12
31
  document.documentElement.setAttribute("data-theme", next);
13
32
  localStorage.setItem("vibespot-theme", next);
33
+ syncThemeToggleLabel();
34
+ }
35
+
36
+ /* ---------------------------------------------------------------------------
37
+ * HubSpot portal indicator (topbar) — visible in both Project Home and Editor
38
+ * Reads /api/settings/status and reflects the active HubSpot portal's
39
+ * connection state. Clicking opens Settings (HubSpot tab).
40
+ * ------------------------------------------------------------------------- */
41
+ async function refreshPortalIndicator() {
42
+ const link = document.getElementById("topbar-portal-indicator");
43
+ const label = document.getElementById("topbar-portal-label");
44
+ if (!link || !label) return;
45
+ try {
46
+ const res = await fetch("/api/settings/status");
47
+ const data = await res.json();
48
+ const hs = data && data.environment && data.environment.tools && data.environment.tools.hubspot;
49
+ if (hs && hs.authenticated && hs.portalName) {
50
+ const portal = hs.portalId ? `${hs.portalName} (${hs.portalId})` : hs.portalName;
51
+ link.classList.add("portal-indicator--connected");
52
+ link.classList.remove("portal-indicator--disconnected");
53
+ link.setAttribute("title", `Connected to HubSpot portal ${portal}`);
54
+ link.setAttribute("aria-label", `Connected to HubSpot portal ${portal}`);
55
+ label.textContent = portal;
56
+ } else {
57
+ link.classList.remove("portal-indicator--connected");
58
+ link.classList.add("portal-indicator--disconnected");
59
+ link.setAttribute("title", "No HubSpot portal connected — open Settings to add one");
60
+ link.setAttribute("aria-label", "No HubSpot portal connected — open Settings to add one");
61
+ label.textContent = "Not connected";
62
+ }
63
+ } catch {
64
+ // Server unreachable — leave the disconnected state in place.
65
+ }
14
66
  }
15
67
 
68
+ function bindPortalIndicator() {
69
+ const link = document.getElementById("topbar-portal-indicator");
70
+ if (!link || link.dataset.bound === "1") return;
71
+ link.dataset.bound = "1";
72
+ link.addEventListener("click", (event) => {
73
+ event.preventDefault();
74
+ // In Editor mode, the workspace tab "settings" exists; otherwise the
75
+ // Project Home settings button opens the same overlay.
76
+ const editorTab = document.getElementById("ws-tab-settings");
77
+ const setupBtn = document.getElementById("btn-setup-settings");
78
+ const appBody = document.getElementById("app-body");
79
+ const isEditor = appBody && appBody.getAttribute("data-mode") === "editor";
80
+ const target = isEditor ? editorTab : setupBtn;
81
+ if (target && typeof target.click === "function") target.click();
82
+ else if (editorTab) editorTab.click();
83
+ else if (setupBtn) setupBtn.click();
84
+ });
85
+ }
86
+
87
+ document.addEventListener("DOMContentLoaded", () => {
88
+ bindPortalIndicator();
89
+ refreshPortalIndicator();
90
+ });
91
+
92
+ // Re-poll occasionally so a freshly-saved PAT shows up without reload.
93
+ setInterval(() => { refreshPortalIndicator(); }, 30000);
94
+
16
95
  /**
17
96
  * Setup screen — onboarding flow in the browser.
18
97
  * Handles theme creation, fetching, opening, and session resume.
19
98
  */
20
99
 
21
100
  const setupScreen = document.getElementById("setup-screen");
22
- const appScreen = document.getElementById("app-screen");
101
+ const appScreen = document.getElementById("editor");
102
+ const appBody = document.getElementById("app-body");
103
+ let _serverContentMode = "page";
23
104
 
24
105
  // ---------------------------------------------------------------------------
25
106
  // Load setup info on page load
@@ -36,11 +117,11 @@ const ENGINE_DISPLAY_NAMES = {
36
117
 
37
118
  async function initSetup() {
38
119
  try {
39
- // Show loading spinner in rail while fetching
40
- const railItems = document.getElementById("project-rail-items");
41
- if (railItems) {
42
- railItems.innerHTML = `
43
- <div class="project-rail__loading">
120
+ // Show loading spinner in switcher while fetching
121
+ const switcherList = document.getElementById("project-switcher-list");
122
+ if (switcherList) {
123
+ switcherList.innerHTML = `
124
+ <div class="project-switcher__loading">
44
125
  <div class="setup__spinner"></div>
45
126
  <span>Loading projects...</span>
46
127
  </div>`;
@@ -49,7 +130,7 @@ async function initSetup() {
49
130
  const res = await fetch("/api/setup");
50
131
  const info = await res.json();
51
132
 
52
- // Populate the project rail with all projects
133
+ // Populate the project switcher with all projects (used in editor mode)
53
134
  populateProjectRail(info);
54
135
 
55
136
  // Show "Continue where you left off" cards above the create options
@@ -96,29 +177,118 @@ async function initSetup() {
96
177
  }, 0);
97
178
  }
98
179
 
99
- // Check if we should show the walkthrough (fresh environment)
180
+ // First-visit product intro: 3-step walkthrough explaining vibeSpot.
181
+ // Add ?intro to URL to force-show it for testing.
182
+ const params = new URLSearchParams(location.search);
183
+ const introSeen = localStorage.getItem(INTRO_SEEN_KEY) === "1";
184
+ const isFreshUser = info.sessions.length === 0 && info.localThemes.length === 0;
185
+ if (params.has("intro") || (!introSeen && isFreshUser)) {
186
+ showIntroWalkthrough(info);
187
+ return;
188
+ }
189
+
190
+ // Check if we should show the engine-setup walkthrough (fresh environment).
100
191
  // Add ?walkthrough to URL to force-show it for testing
101
- if (new URLSearchParams(location.search).has("walkthrough") ||
102
- (!info.aiAvailable && info.sessions.length === 0 && info.localThemes.length === 0)) {
192
+ if (params.has("walkthrough") ||
193
+ (!info.aiAvailable && isFreshUser)) {
103
194
  showWalkthrough();
104
195
  return;
105
196
  }
106
197
 
198
+ // Track server content mode (email vs page)
199
+ _serverContentMode = info.contentMode || "page";
200
+
107
201
  // Reset panel state
108
202
  remoteThemesLoaded = false;
109
203
 
110
204
  // Reset starter cache so each visit re-fetches from server
111
205
  _startersCache = null;
112
206
 
113
- // Auto-expand the starter template panel so templates are visible by default
207
+ // Set warm time-of-day greeting; show asset-type cards as the primary entry.
208
+ initGuidedEntry();
209
+
210
+ // Reset to cards-first state (no panel, prompt hidden).
114
211
  activePanel = null;
115
- togglePanel("starter");
212
+ showAssetTypeCards();
116
213
 
117
214
  } catch (err) {
118
215
  showError("Could not connect to server. Is vibeSpot running?");
119
216
  }
120
217
  }
121
218
 
219
+ // ---------------------------------------------------------------------------
220
+ // Guided entry — time-of-day greeting + asset-type card flow (VIB-255)
221
+ // ---------------------------------------------------------------------------
222
+
223
+ function initGuidedEntry() {
224
+ const textEl = document.getElementById("setup-greeting-text");
225
+ if (!textEl) return;
226
+ const hour = new Date().getHours();
227
+ let greeting = "Welcome";
228
+ if (hour < 5) greeting = "Working late";
229
+ else if (hour < 12) greeting = "Good morning";
230
+ else if (hour < 17) greeting = "Good afternoon";
231
+ else greeting = "Good evening";
232
+ textEl.textContent = greeting;
233
+ }
234
+
235
+ let _selectedAssetType = null;
236
+
237
+ function showAssetTypeCards() {
238
+ const cards = document.getElementById("setup-type-cards");
239
+ const promptCard = document.getElementById("setup-prompt-card");
240
+ const recent = document.getElementById("setup-recent");
241
+ const promptInput = document.getElementById("setup-prompt-input");
242
+ const question = document.getElementById("setup-question");
243
+ const importPanel = document.getElementById("setup-import-sources");
244
+ if (cards) cards.classList.remove("hidden");
245
+ if (question) question.classList.remove("hidden");
246
+ if (importPanel) importPanel.classList.add("hidden");
247
+ if (promptCard) {
248
+ promptCard.classList.add("hidden");
249
+ promptCard.dataset.assetType = "";
250
+ }
251
+ if (recent && recent.dataset.hasItems === "1") recent.classList.remove("hidden");
252
+ if (promptInput) {
253
+ promptInput.value = "";
254
+ const submit = document.getElementById("setup-prompt-submit");
255
+ if (submit) submit.disabled = true;
256
+ }
257
+ // Close any open advanced panel (e.g. starter grid) when returning to cards.
258
+ document.querySelectorAll(".setup__panel").forEach((p) => p.classList.add("hidden"));
259
+ document.querySelectorAll(".setup__action-btn").forEach((b) => b.classList.remove("active"));
260
+ activePanel = null;
261
+ _selectedAssetType = null;
262
+ }
263
+
264
+ function showScopedPrompt(card) {
265
+ const cards = document.getElementById("setup-type-cards");
266
+ const promptCard = document.getElementById("setup-prompt-card");
267
+ const recent = document.getElementById("setup-recent");
268
+ const eyebrow = document.getElementById("setup-prompt-eyebrow");
269
+ const input = document.getElementById("setup-prompt-input");
270
+ if (!promptCard || !input) return;
271
+
272
+ const assetType = card.dataset.assetType || "landing-page";
273
+ const placeholder = card.dataset.promptPlaceholder || "Describe what you want to build...";
274
+ const label = card.dataset.promptEyebrow || "Project";
275
+
276
+ _selectedAssetType = assetType;
277
+ promptCard.dataset.assetType = assetType;
278
+ if (eyebrow) eyebrow.textContent = label;
279
+ input.placeholder = placeholder;
280
+ input.setAttribute("aria-label", placeholder.replace(/\.\.\.$/, ""));
281
+
282
+ // Stash the selected type so downstream session/chat code can read it later.
283
+ window.__pendingAssetType = assetType;
284
+
285
+ if (cards) cards.classList.add("hidden");
286
+ if (recent) recent.classList.add("hidden");
287
+ promptCard.classList.remove("hidden");
288
+
289
+ setTimeout(() => input.focus(), 60);
290
+ }
291
+
122
292
  async function saveAlertApiKey(key) {
123
293
  if (!key) return;
124
294
  // Detect provider from key prefix
@@ -169,6 +339,9 @@ function deduplicateProjects(info) {
169
339
  updatedAt: s.updatedAt,
170
340
  moduleCount: s.moduleCount ?? null,
171
341
  templateCount: s.templateCount ?? null,
342
+ pageCount: s.pageCount ?? 0,
343
+ emailCount: s.emailCount ?? 0,
344
+ hasBrandAssets: s.hasBrandAssets ?? false,
172
345
  });
173
346
  }
174
347
  }
@@ -184,6 +357,9 @@ function deduplicateProjects(info) {
184
357
  updatedAt: null,
185
358
  moduleCount: typeof t === "object" ? t.moduleCount ?? null : null,
186
359
  templateCount: null,
360
+ pageCount: 0,
361
+ emailCount: 0,
362
+ hasBrandAssets: false,
187
363
  });
188
364
  }
189
365
  }
@@ -196,6 +372,7 @@ function deduplicateProjects(info) {
196
372
  // ---------------------------------------------------------------------------
197
373
 
198
374
  const RECENT_PROJECTS_LIMIT = 4;
375
+ let _allProjects = [];
199
376
 
200
377
  function populateRecentProjects(info) {
201
378
  const section = document.getElementById("setup-recent");
@@ -205,15 +382,19 @@ function populateRecentProjects(info) {
205
382
 
206
383
  const projects = deduplicateProjects(info);
207
384
  if (projects.length === 0) {
385
+ _allProjects = [];
208
386
  section.classList.add("hidden");
387
+ section.dataset.hasItems = "0";
209
388
  list.innerHTML = "";
210
389
  return;
211
390
  }
391
+ section.dataset.hasItems = "1";
212
392
 
213
393
  // Most recently updated first; locals (no updatedAt) follow
214
394
  const withTime = projects.filter((p) => p.updatedAt).sort((a, b) => b.updatedAt - a.updatedAt);
215
395
  const withoutTime = projects.filter((p) => !p.updatedAt);
216
396
  const ordered = [...withTime, ...withoutTime];
397
+ _allProjects = ordered;
217
398
  const top = ordered.slice(0, RECENT_PROJECTS_LIMIT);
218
399
 
219
400
  list.innerHTML = "";
@@ -232,6 +413,17 @@ function populateRecentProjects(info) {
232
413
  `<span class="setup__recent-card-meta">${esc(meta)}</span>` +
233
414
  `</span>`;
234
415
 
416
+ const delBtn = document.createElement("button");
417
+ delBtn.type = "button";
418
+ delBtn.className = "setup__recent-card-delete";
419
+ delBtn.innerHTML = "&times;";
420
+ delBtn.title = "Delete project";
421
+ delBtn.addEventListener("click", (e) => {
422
+ e.stopPropagation();
423
+ confirmDeleteProject(p);
424
+ });
425
+ card.appendChild(delBtn);
426
+
235
427
  card.addEventListener("click", () => {
236
428
  if (typeof isStreaming !== "undefined" && isStreaming) {
237
429
  showError("Cannot switch projects while AI is generating.");
@@ -248,40 +440,43 @@ function populateRecentProjects(info) {
248
440
  }
249
441
 
250
442
  // ---------------------------------------------------------------------------
251
- // Collapsible Project Rail (expanded on setup, collapsed on dashboard/chat)
443
+ // Project switcher (rendered into the editor-mode rail popover)
252
444
  // ---------------------------------------------------------------------------
253
445
 
254
446
  const railTooltip = document.getElementById("project-rail-tooltip");
255
447
 
448
+ /**
449
+ * Populate the project switcher menu with all projects. Item DOM uses the
450
+ * stable `.project-rail__item*` class names so the rename / delete logic in
451
+ * chat.js + setup.js can keep targeting them.
452
+ */
256
453
  function populateProjectRail(info) {
257
- const rail = document.getElementById("project-rail-items");
258
- const countEl = document.getElementById("rail-project-count");
259
- if (!rail) return;
260
- rail.innerHTML = "";
454
+ const list = document.getElementById("project-switcher-list");
455
+ if (!list) return;
456
+ list.innerHTML = "";
261
457
 
262
458
  const projects = deduplicateProjects(info);
263
- if (countEl) countEl.textContent = projects.length;
264
459
 
265
460
  if (projects.length === 0) {
266
- rail.innerHTML = '<div class="project-rail__empty">No projects yet.<br>Create one to get started.</div>';
461
+ list.innerHTML = '<div class="project-switcher__empty">No projects yet.<br>Create one to get started.</div>';
267
462
  return;
268
463
  }
269
464
 
270
465
  for (const p of projects) {
271
- const item = document.createElement("div");
466
+ const item = document.createElement("button");
467
+ item.type = "button";
272
468
  item.className = "project-rail__item";
273
469
  item.dataset.name = p.name;
470
+ if (p.sessionId) item.dataset.sessionId = p.sessionId;
274
471
 
275
472
  const initial = p.name.charAt(0).toUpperCase();
276
473
  const meta = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
277
474
 
278
- // Bubble (always visible — in collapsed mode this is the only thing shown)
279
475
  const bubble = document.createElement("div");
280
476
  bubble.className = "project-rail__item-bubble";
281
477
  bubble.textContent = initial;
282
478
  item.appendChild(bubble);
283
479
 
284
- // Info (visible when expanded via CSS)
285
480
  const infoEl = document.createElement("div");
286
481
  infoEl.className = "project-rail__item-info";
287
482
  infoEl.innerHTML = `
@@ -289,7 +484,6 @@ function populateProjectRail(info) {
289
484
  <span class="project-rail__item-meta">${meta}</span>`;
290
485
  item.appendChild(infoEl);
291
486
 
292
- // Double-click on name to rename
293
487
  const nameSpan = infoEl.querySelector(".project-rail__item-name");
294
488
  if (nameSpan) {
295
489
  nameSpan.addEventListener("dblclick", (e) => {
@@ -298,8 +492,8 @@ function populateProjectRail(info) {
298
492
  });
299
493
  }
300
494
 
301
- // Delete button (visible when expanded + hover)
302
495
  const delBtn = document.createElement("button");
496
+ delBtn.type = "button";
303
497
  delBtn.className = "project-rail__item-delete";
304
498
  delBtn.innerHTML = "&times;";
305
499
  delBtn.title = "Delete project";
@@ -309,44 +503,17 @@ function populateProjectRail(info) {
309
503
  });
310
504
  item.appendChild(delBtn);
311
505
 
312
- // Tooltip (only when collapsed — skip when expanded since name is visible)
313
- item.addEventListener("mouseenter", () => {
314
- const railEl = document.getElementById("project-rail");
315
- if (railEl && railEl.classList.contains("project-rail--expanded")) return;
316
-
317
- let stats = "";
318
- if (p.moduleCount != null) {
319
- stats = p.moduleCount + " section" + (p.moduleCount !== 1 ? "s" : "");
320
- if (p.templateCount > 1) stats += " \u00b7 " + p.templateCount + " templates";
321
- stats += p.updatedAt ? " \u00b7 " + timeAgo(p.updatedAt) : " \u00b7 on disk";
322
- } else {
323
- stats = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
324
- }
325
-
326
- railTooltip.innerHTML =
327
- '<div class="project-rail__tooltip-name">' + esc(p.name) + "</div>" +
328
- '<div class="project-rail__tooltip-stats">' + stats + "</div>";
329
-
330
- const rect = item.getBoundingClientRect();
331
- railTooltip.style.top = rect.top + "px";
332
- railTooltip.classList.add("project-rail__tooltip--visible");
333
- });
334
-
335
- item.addEventListener("mouseleave", () => {
336
- railTooltip.classList.remove("project-rail__tooltip--visible");
337
- });
338
-
339
- // Click to open (blocked while AI is generating)
340
506
  item.addEventListener("click", () => {
341
507
  if (typeof isStreaming !== "undefined" && isStreaming) {
342
508
  showError("Cannot switch projects while AI is generating.");
343
509
  return;
344
510
  }
511
+ closeProjectSwitcher();
345
512
  if (p.sessionId) resumeSession(p.sessionId);
346
513
  else openTheme(p.name);
347
514
  });
348
515
 
349
- rail.appendChild(item);
516
+ list.appendChild(item);
350
517
  }
351
518
 
352
519
  updateRailActive();
@@ -357,14 +524,99 @@ function updateRailActive() {
357
524
  document.querySelectorAll(".project-rail__item").forEach((btn) => {
358
525
  btn.classList.toggle("project-rail__item--active", btn.dataset.name === current);
359
526
  });
527
+ // Refresh the rail's current-project bubble + name (editor mode only).
528
+ const bubble = document.getElementById("project-rail-current-bubble");
529
+ const nameEl = document.getElementById("project-rail-current-name");
530
+ if (bubble) bubble.textContent = current ? current.charAt(0).toUpperCase() : "P";
531
+ if (nameEl) nameEl.textContent = current || "";
532
+ const trigger = document.getElementById("project-rail-current");
533
+ if (trigger) trigger.title = current ? current + " — switch project" : "Switch project";
534
+ }
535
+
536
+ // ---------------------------------------------------------------------------
537
+ // Switcher popover open/close
538
+ // ---------------------------------------------------------------------------
539
+
540
+ function openProjectSwitcher() {
541
+ const popover = document.getElementById("project-switcher");
542
+ const trigger = document.getElementById("project-rail-current");
543
+ if (!popover || !trigger) return;
544
+ const rect = trigger.getBoundingClientRect();
545
+ popover.style.top = Math.max(8, rect.top) + "px";
546
+ popover.hidden = false;
547
+ trigger.setAttribute("aria-expanded", "true");
548
+ // Refresh data so the list reflects the latest sessions.
549
+ fetch("/api/setup")
550
+ .then((r) => r.json())
551
+ .then((info) => populateProjectRail(info))
552
+ .catch(() => {});
553
+ }
554
+
555
+ function closeProjectSwitcher() {
556
+ const popover = document.getElementById("project-switcher");
557
+ const trigger = document.getElementById("project-rail-current");
558
+ if (popover) popover.hidden = true;
559
+ if (trigger) trigger.setAttribute("aria-expanded", "false");
560
+ }
561
+
562
+ function toggleProjectSwitcher() {
563
+ const popover = document.getElementById("project-switcher");
564
+ if (!popover) return;
565
+ if (popover.hidden) openProjectSwitcher();
566
+ else closeProjectSwitcher();
360
567
  }
361
568
 
362
- // "+" button → open New Theme panel (show setup first if needed)
363
- document.getElementById("project-rail-add")?.addEventListener("click", () => {
364
- if (setupScreen.classList.contains("hidden")) showSetup();
569
+ document.getElementById("project-rail-current")?.addEventListener("click", (e) => {
570
+ e.stopPropagation();
571
+ toggleProjectSwitcher();
572
+ });
573
+
574
+ document.getElementById("project-rail-back")?.addEventListener("click", () => {
575
+ if (typeof isStreaming !== "undefined" && isStreaming) {
576
+ showError("Cannot leave the editor while AI is generating.");
577
+ return;
578
+ }
579
+ closeProjectSwitcher();
580
+ showSetup();
581
+ });
582
+
583
+ document.getElementById("project-switcher-add")?.addEventListener("click", () => {
584
+ closeProjectSwitcher();
585
+ showSetup();
365
586
  togglePanel("new");
366
587
  });
367
588
 
589
+ // Close on outside click / Escape
590
+ document.addEventListener("click", (e) => {
591
+ const popover = document.getElementById("project-switcher");
592
+ if (!popover || popover.hidden) return;
593
+ const trigger = document.getElementById("project-rail-current");
594
+ if (popover.contains(e.target) || (trigger && trigger.contains(e.target))) return;
595
+ closeProjectSwitcher();
596
+ });
597
+
598
+ document.addEventListener("keydown", (e) => {
599
+ if (e.key === "Escape") closeProjectSwitcher();
600
+ });
601
+
602
+ // Tooltip on the current-project bubble (editor rail)
603
+ document.getElementById("project-rail-current")?.addEventListener("mouseenter", () => {
604
+ const popover = document.getElementById("project-switcher");
605
+ if (popover && !popover.hidden) return;
606
+ const name = currentAppTheme || currentDashboardTheme || "";
607
+ if (!name) return;
608
+ railTooltip.innerHTML =
609
+ '<div class="project-rail__tooltip-name">' + esc(name) + "</div>" +
610
+ '<div class="project-rail__tooltip-stats">Click to switch project</div>';
611
+ const rect = document.getElementById("project-rail-current").getBoundingClientRect();
612
+ railTooltip.style.top = rect.top + "px";
613
+ railTooltip.classList.add("project-rail__tooltip--visible");
614
+ });
615
+
616
+ document.getElementById("project-rail-current")?.addEventListener("mouseleave", () => {
617
+ railTooltip.classList.remove("project-rail__tooltip--visible");
618
+ });
619
+
368
620
  // ---------------------------------------------------------------------------
369
621
  // Inline rename
370
622
  // ---------------------------------------------------------------------------
@@ -497,6 +749,146 @@ function confirmDeleteProject(project) {
497
749
  });
498
750
  }
499
751
 
752
+ // ---------------------------------------------------------------------------
753
+ // First-visit product intro walkthrough (3 steps)
754
+ // ---------------------------------------------------------------------------
755
+
756
+ const INTRO_SEEN_KEY = "vibespot:introSeen";
757
+ const INTRO_SAMPLE_PROMPT =
758
+ "A landing page for a B2B SaaS product called Northwind Analytics. " +
759
+ "Include a hero with a headline and CTA, three feature cards, a customer logo bar, " +
760
+ "a testimonial, and a final call-to-action section.";
761
+
762
+ function renderIntroProgress(stepIndex, totalSteps) {
763
+ let html = "";
764
+ for (let i = 0; i < totalSteps; i++) {
765
+ const cls = i === stepIndex ? "active" : i < stepIndex ? "done" : "";
766
+ html += `<div class="walkthrough__step-dot ${cls}">${i < stepIndex ? vsIcon("check", {size: "sm"}) : i + 1}</div>`;
767
+ if (i < totalSteps - 1) html += `<div class="walkthrough__step-line"></div>`;
768
+ }
769
+ return html;
770
+ }
771
+
772
+ function dismissIntroWalkthrough(info) {
773
+ try { localStorage.setItem(INTRO_SEEN_KEY, "1"); } catch {}
774
+ const url = new URL(location.href);
775
+ if (url.searchParams.has("intro")) {
776
+ url.searchParams.delete("intro");
777
+ history.replaceState(null, "", url.pathname + url.search + url.hash);
778
+ }
779
+ document.getElementById("walkthrough").classList.add("hidden");
780
+ // If the user still has no AI engine on a fresh install, fall through to
781
+ // the engine-setup walkthrough; otherwise reveal the normal setup options.
782
+ if (info && !info.aiAvailable && (info.sessions || []).length === 0 && (info.localThemes || []).length === 0) {
783
+ showWalkthrough();
784
+ } else {
785
+ document.getElementById("setup-options").classList.remove("hidden");
786
+ }
787
+ }
788
+
789
+ function showIntroWalkthrough(info) {
790
+ const walkthrough = document.getElementById("walkthrough");
791
+ const options = document.getElementById("setup-options");
792
+ const progress = document.getElementById("walkthrough-progress");
793
+ const content = document.getElementById("walkthrough-content");
794
+ if (!walkthrough || !options || !progress || !content) return;
795
+
796
+ walkthrough.classList.remove("hidden");
797
+ options.classList.add("hidden");
798
+
799
+ const STEPS = [
800
+ {
801
+ title: "Welcome to vibeSpot",
802
+ body: `
803
+ <p>vibeSpot turns plain-language descriptions into native HubSpot CMS landing pages.
804
+ Describe what you want, watch a live preview build, then upload the result straight to HubSpot.</p>
805
+ <ul class="walkthrough__bullets">
806
+ <li>Chat-driven editing with a side-by-side preview</li>
807
+ <li>Generates real HubL modules, not screenshots or mockups</li>
808
+ <li>Works with Claude, OpenAI, or Gemini &mdash; API key or CLI</li>
809
+ </ul>
810
+ `,
811
+ },
812
+ {
813
+ title: "How it maps to HubSpot",
814
+ body: `
815
+ <p>Every vibeSpot page becomes a fully editable HubSpot theme. The pieces line up like this:</p>
816
+ <ul class="walkthrough__bullets">
817
+ <li><strong>Sections</strong> &rarr; HubSpot <strong>modules</strong> with editable fields</li>
818
+ <li><strong>Shared CSS &amp; tokens</strong> &rarr; theme-level <code>:root</code> variables</li>
819
+ <li><strong>Project</strong> &rarr; uploadable <strong>HubSpot CMS theme</strong> with templates</li>
820
+ </ul>
821
+ <p>Marketers can keep editing fields in HubSpot after upload &mdash; no code changes required.</p>
822
+ `,
823
+ },
824
+ {
825
+ title: "Try it with a pre-filled prompt",
826
+ body: `
827
+ <p>We&rsquo;ll drop a sample prompt into the builder so you can see vibeSpot in action.
828
+ You can edit it before pressing <strong>Build</strong>.</p>
829
+ <div class="walkthrough__card walkthrough__sample-prompt">
830
+ <div class="walkthrough__card-title">Sample prompt</div>
831
+ <div class="walkthrough__sample-prompt-body">${esc(INTRO_SAMPLE_PROMPT)}</div>
832
+ </div>
833
+ `,
834
+ },
835
+ ];
836
+
837
+ let stepIndex = 0;
838
+
839
+ function render() {
840
+ const step = STEPS[stepIndex];
841
+ progress.innerHTML = renderIntroProgress(stepIndex, STEPS.length);
842
+
843
+ const isLast = stepIndex === STEPS.length - 1;
844
+ const primaryLabel = isLast ? "Try it now" : "Next";
845
+ const backBtn = stepIndex > 0
846
+ ? `<button class="btn btn--secondary" id="intro-back">Back</button>`
847
+ : "";
848
+
849
+ content.innerHTML = `
850
+ <div class="walkthrough__step-title">${esc(step.title)}</div>
851
+ <div class="walkthrough__step-desc">${step.body}</div>
852
+ <div class="walkthrough__actions">
853
+ <button class="btn btn--ghost" id="intro-skip">Skip intro</button>
854
+ <span class="walkthrough__actions-spacer"></span>
855
+ ${backBtn}
856
+ <button class="btn btn--primary" id="intro-next">${primaryLabel}</button>
857
+ </div>
858
+ `;
859
+
860
+ document.getElementById("intro-skip").addEventListener("click", () => dismissIntroWalkthrough(info));
861
+ document.getElementById("intro-next").addEventListener("click", () => {
862
+ if (isLast) {
863
+ // Pre-fill the prompt and hand off to the normal builder flow.
864
+ const promptInput = document.getElementById("setup-prompt-input");
865
+ if (promptInput) {
866
+ promptInput.value = INTRO_SAMPLE_PROMPT;
867
+ promptInput.dispatchEvent(new Event("input", { bubbles: true }));
868
+ }
869
+ dismissIntroWalkthrough(info);
870
+ // Focus the prompt so the user lands ready to edit / submit.
871
+ setTimeout(() => {
872
+ const el = document.getElementById("setup-prompt-input");
873
+ if (el) {
874
+ el.focus();
875
+ if (typeof el.scrollIntoView === "function") {
876
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
877
+ }
878
+ }
879
+ }, 0);
880
+ return;
881
+ }
882
+ stepIndex++;
883
+ render();
884
+ });
885
+ const backEl = document.getElementById("intro-back");
886
+ if (backEl) backEl.addEventListener("click", () => { stepIndex--; render(); });
887
+ }
888
+
889
+ render();
890
+ }
891
+
500
892
  // ---------------------------------------------------------------------------
501
893
  // Guided walkthrough (first-run experience)
502
894
  // ---------------------------------------------------------------------------
@@ -765,10 +1157,12 @@ async function startFromPrompt() {
765
1157
  showLoading("Creating theme...");
766
1158
 
767
1159
  try {
1160
+ const createBody = { name: themeName };
1161
+ if (window.__pendingAssetType) createBody.assetType = window.__pendingAssetType;
768
1162
  const res = await fetch("/api/setup/create", {
769
1163
  method: "POST",
770
1164
  headers: { "Content-Type": "application/json" },
771
- body: JSON.stringify({ name: themeName }),
1165
+ body: JSON.stringify(createBody),
772
1166
  });
773
1167
  const data = await res.json();
774
1168
 
@@ -833,7 +1227,10 @@ function renderStarterGrid(starters) {
833
1227
  return;
834
1228
  }
835
1229
 
836
- grid.innerHTML = starters.map((s) => `
1230
+ const pageStarters = starters.filter((s) => s.contentType !== "email");
1231
+ const emailStarters = starters.filter((s) => s.contentType === "email");
1232
+
1233
+ const renderCards = (list) => list.map((s) => `
837
1234
  <div class="starter-card${_selectedStarterId === s.id ? " selected" : ""}" data-starter-id="${escHtml(s.id)}">
838
1235
  <span class="starter-card__name">${escHtml(s.name)}</span>
839
1236
  <span class="starter-card__desc">${escHtml(s.description)}</span>
@@ -841,6 +1238,22 @@ function renderStarterGrid(starters) {
841
1238
  </div>
842
1239
  `).join("");
843
1240
 
1241
+ const renderGroup = (title, list) =>
1242
+ `<div class="starter-grid__group">
1243
+ <h4 class="starter-grid__heading">${escHtml(title)}</h4>
1244
+ <div class="starter-grid__section">${renderCards(list)}</div>
1245
+ </div>`;
1246
+
1247
+ let html = "";
1248
+ if (_serverContentMode === "email") {
1249
+ if (emailStarters.length > 0) html += renderGroup("Email Templates", emailStarters);
1250
+ if (pageStarters.length > 0) html += renderGroup("Page Templates", pageStarters);
1251
+ } else {
1252
+ if (pageStarters.length > 0) html += renderGroup("Page Templates", pageStarters);
1253
+ if (emailStarters.length > 0) html += renderGroup("Email Templates", emailStarters);
1254
+ }
1255
+ grid.innerHTML = html;
1256
+
844
1257
  grid.querySelectorAll(".starter-card").forEach((card) => {
845
1258
  card.addEventListener("click", () => selectStarter(card.dataset.starterId));
846
1259
  });
@@ -892,7 +1305,8 @@ async function createFromStarter() {
892
1305
  }
893
1306
 
894
1307
  async function fetchTheme() {
895
- const name = document.getElementById("fetch-theme-name").value.trim();
1308
+ const nameEl = document.getElementById("fetch-theme-name") || document.getElementById("dl-theme-name");
1309
+ const name = nameEl ? nameEl.value.trim() : "";
896
1310
  if (!name) {
897
1311
  showError("Please enter the theme name from your HubSpot account.");
898
1312
  return;
@@ -997,16 +1411,12 @@ function showApp(themeName) {
997
1411
  * Used as fallback or when navigating from dashboard to a specific template.
998
1412
  */
999
1413
  function showAppDirect(themeName) {
1000
- setupScreen.classList.add("hidden");
1001
- document.getElementById("setup-topbar").classList.add("hidden");
1002
- document.getElementById("project-rail")?.classList.remove("project-rail--expanded");
1003
1414
  if (typeof hideDashboard === "function") hideDashboard();
1415
+ appBody.dataset.mode = "editor";
1004
1416
  appScreen.classList.remove("hidden");
1417
+ document.getElementById("project-rail")?.setAttribute("data-mode", "editor");
1005
1418
  document.getElementById("theme-name").textContent = themeName;
1006
1419
 
1007
- const urlEl = document.getElementById("browser-url");
1008
- if (urlEl) urlEl.textContent = themeName + ".vibespot.app";
1009
-
1010
1420
  currentAppTheme = themeName;
1011
1421
  const target = "#/app/" + encodeURIComponent(themeName);
1012
1422
  if (location.hash !== target) {
@@ -1025,9 +1435,9 @@ function showAppDirect(themeName) {
1025
1435
  function showSetup() {
1026
1436
  appScreen.classList.add("hidden");
1027
1437
  if (typeof hideDashboard === "function") hideDashboard();
1028
- setupScreen.classList.remove("hidden");
1029
- document.getElementById("setup-topbar").classList.remove("hidden");
1030
- document.getElementById("project-rail")?.classList.add("project-rail--expanded");
1438
+ appBody.dataset.mode = "project-home";
1439
+ document.getElementById("project-rail")?.setAttribute("data-mode", "project-home");
1440
+ closeProjectSwitcher();
1031
1441
  currentAppTheme = "";
1032
1442
 
1033
1443
  hideLoading();
@@ -1040,25 +1450,16 @@ function showSetup() {
1040
1450
  initSetup();
1041
1451
  }
1042
1452
 
1043
- // App back button → go back to dashboard from chat
1044
- document.getElementById("app-back")?.addEventListener("click", () => {
1045
- if (currentAppTheme && typeof showDashboard === "function") {
1046
- appScreen.classList.add("hidden");
1047
- showDashboard(currentAppTheme);
1048
- }
1453
+ // Editor back button → go back to setup
1454
+ document.getElementById("editor-back")?.addEventListener("click", () => {
1455
+ showSetup();
1049
1456
  });
1050
1457
 
1051
- // Logo click → go back to setup (from dashboard)
1458
+ // Logo click → go back to setup
1052
1459
  document.querySelectorAll(".topbar__brand").forEach((el) => {
1053
1460
  el.style.cursor = "pointer";
1054
1461
  el.addEventListener("click", () => {
1055
- const dashEl = document.getElementById("dashboard-screen");
1056
- if (dashEl && !dashEl.classList.contains("hidden")) {
1057
- showSetup();
1058
- return;
1059
- }
1060
- // Fallback
1061
- if (!appScreen.classList.contains("hidden")) {
1462
+ if (appBody.dataset.mode === "editor") {
1062
1463
  showSetup();
1063
1464
  }
1064
1465
  });
@@ -1097,7 +1498,7 @@ let remoteThemesLoaded = false;
1097
1498
 
1098
1499
  function togglePanel(action) {
1099
1500
  const panels = document.querySelectorAll(".setup__panel");
1100
- const buttons = document.querySelectorAll(".setup__action-btn");
1501
+ const buttons = document.querySelectorAll(".setup__entry-card");
1101
1502
 
1102
1503
  // Close if same panel clicked
1103
1504
  if (activePanel === action) {
@@ -1118,8 +1519,8 @@ function togglePanel(action) {
1118
1519
  activePanel = action;
1119
1520
  }
1120
1521
 
1121
- // Mark button active
1122
- const btn = document.querySelector(`.setup__action-btn[data-action="${action}"]`);
1522
+ // Mark card active
1523
+ const btn = document.querySelector(`.setup__entry-card[data-action="${action}"]`);
1123
1524
  if (btn) btn.classList.add("active");
1124
1525
 
1125
1526
  // Focus input if applicable
@@ -1135,39 +1536,225 @@ function togglePanel(action) {
1135
1536
  if (action === "continue") populateContinuePanel();
1136
1537
  }
1137
1538
 
1539
+ let _bulkSelected = new Set();
1540
+
1138
1541
  function populateContinuePanel() {
1139
1542
  const container = document.getElementById("continue-projects");
1140
1543
  const empty = document.getElementById("continue-empty");
1141
1544
  if (!container) return;
1142
1545
 
1143
- // Gather projects from the rail
1144
- const railItems = document.querySelectorAll(".project-rail__item");
1145
- if (railItems.length === 0) {
1546
+ const projects = _allProjects;
1547
+ if (!projects || projects.length === 0) {
1146
1548
  container.innerHTML = "";
1147
- empty.classList.remove("hidden");
1549
+ _bulkSelected.clear();
1550
+ if (empty) empty.classList.remove("hidden");
1148
1551
  return;
1149
1552
  }
1150
1553
 
1151
- empty.classList.add("hidden");
1152
- container.innerHTML = "";
1554
+ if (empty) empty.classList.add("hidden");
1555
+ _bulkSelected.clear();
1556
+
1557
+ const toolbar = document.createElement("div");
1558
+ toolbar.className = "projects-bulk-toolbar hidden";
1559
+ toolbar.id = "projects-bulk-toolbar";
1560
+ toolbar.innerHTML =
1561
+ `<span class="projects-bulk-toolbar__count" id="bulk-count">0 selected</span>` +
1562
+ `<button type="button" class="btn btn--sm btn--secondary" id="bulk-duplicate">Duplicate</button>` +
1563
+ `<button type="button" class="btn btn--sm btn--danger" id="bulk-delete">Delete</button>`;
1564
+
1565
+ const table = document.createElement("table");
1566
+ table.className = "projects-table";
1567
+ table.innerHTML = `<thead><tr>
1568
+ <th class="projects-table__th-check"><input type="checkbox" id="bulk-select-all" class="projects-table__checkbox" title="Select all" /></th>
1569
+ <th>Name</th>
1570
+ <th>Pages</th>
1571
+ <th>Emails</th>
1572
+ <th>Modules</th>
1573
+ <th>Brand Assets</th>
1574
+ <th></th>
1575
+ </tr></thead>`;
1576
+
1577
+ const tbody = document.createElement("tbody");
1578
+ for (const p of projects) {
1579
+ const tr = document.createElement("tr");
1580
+ tr.dataset.projectName = p.name;
1581
+
1582
+ const checkTd = document.createElement("td");
1583
+ checkTd.className = "projects-table__td-check";
1584
+ const cb = document.createElement("input");
1585
+ cb.type = "checkbox";
1586
+ cb.className = "projects-table__checkbox";
1587
+ cb.dataset.projectName = p.name;
1588
+ cb.addEventListener("change", () => {
1589
+ if (cb.checked) _bulkSelected.add(p.name);
1590
+ else _bulkSelected.delete(p.name);
1591
+ tr.classList.toggle("projects-table__row--selected", cb.checked);
1592
+ syncBulkToolbar();
1593
+ });
1594
+ checkTd.appendChild(cb);
1595
+ tr.appendChild(checkTd);
1596
+
1597
+ const nameTd = document.createElement("td");
1598
+ nameTd.className = "projects-table__name";
1599
+ nameTd.textContent = p.name;
1600
+ tr.appendChild(nameTd);
1601
+
1602
+ for (const val of [p.pageCount ?? 0, p.emailCount ?? 0, p.moduleCount ?? 0]) {
1603
+ const td = document.createElement("td");
1604
+ td.textContent = String(val);
1605
+ tr.appendChild(td);
1606
+ }
1607
+
1608
+ const brandTd = document.createElement("td");
1609
+ brandTd.textContent = p.hasBrandAssets ? "✓" : "—";
1610
+ tr.appendChild(brandTd);
1611
+
1612
+ const actionsCell = document.createElement("td");
1613
+ actionsCell.className = "projects-table__actions";
1614
+ tr.appendChild(actionsCell);
1153
1615
 
1154
- railItems.forEach((item) => {
1155
- const name = item.dataset.name || item.querySelector(".project-rail__name")?.textContent || "";
1156
- const sessionId = item.dataset.sessionId || "";
1157
- const meta = item.querySelector(".project-rail__meta")?.textContent || "";
1158
-
1159
- const pill = document.createElement("button");
1160
- pill.className = "setup__pill";
1161
- pill.innerHTML = `<span>${esc(name)}</span>${meta ? `<span class="setup__pill__meta">${esc(meta)}</span>` : ""}`;
1162
- pill.addEventListener("click", () => {
1163
- if (sessionId) {
1164
- resumeSession(sessionId);
1165
- } else {
1166
- openTheme(name);
1616
+ const openBtn = document.createElement("button");
1617
+ openBtn.type = "button";
1618
+ openBtn.className = "btn btn--sm btn--primary";
1619
+ openBtn.textContent = "Open";
1620
+ openBtn.addEventListener("click", () => {
1621
+ if (typeof isStreaming !== "undefined" && isStreaming) {
1622
+ showError("Cannot switch projects while AI is generating.");
1623
+ return;
1167
1624
  }
1625
+ if (p.sessionId) resumeSession(p.sessionId);
1626
+ else openTheme(p.name);
1627
+ });
1628
+ actionsCell.appendChild(openBtn);
1629
+
1630
+ const delBtn = document.createElement("button");
1631
+ delBtn.type = "button";
1632
+ delBtn.className = "btn btn--sm btn--danger";
1633
+ delBtn.textContent = "Delete";
1634
+ delBtn.addEventListener("click", () => {
1635
+ confirmDeleteProject(p);
1168
1636
  });
1169
- container.appendChild(pill);
1637
+ actionsCell.appendChild(delBtn);
1638
+
1639
+ tbody.appendChild(tr);
1640
+ }
1641
+ table.appendChild(tbody);
1642
+
1643
+ container.innerHTML = "";
1644
+ container.appendChild(toolbar);
1645
+ container.appendChild(table);
1646
+
1647
+ const selectAll = document.getElementById("bulk-select-all");
1648
+ selectAll.addEventListener("change", () => {
1649
+ const cbs = container.querySelectorAll("tbody .projects-table__checkbox");
1650
+ cbs.forEach((c) => {
1651
+ c.checked = selectAll.checked;
1652
+ const name = c.dataset.projectName;
1653
+ const row = c.closest("tr");
1654
+ if (selectAll.checked) _bulkSelected.add(name);
1655
+ else _bulkSelected.delete(name);
1656
+ if (row) row.classList.toggle("projects-table__row--selected", selectAll.checked);
1657
+ });
1658
+ syncBulkToolbar();
1170
1659
  });
1660
+
1661
+ document.getElementById("bulk-delete").addEventListener("click", () => bulkDeleteProjects());
1662
+ document.getElementById("bulk-duplicate").addEventListener("click", () => bulkDuplicateProjects());
1663
+ }
1664
+
1665
+ function syncBulkToolbar() {
1666
+ const toolbar = document.getElementById("projects-bulk-toolbar");
1667
+ const countEl = document.getElementById("bulk-count");
1668
+ const selectAll = document.getElementById("bulk-select-all");
1669
+ if (!toolbar) return;
1670
+
1671
+ const n = _bulkSelected.size;
1672
+ toolbar.classList.toggle("hidden", n === 0);
1673
+ if (countEl) countEl.textContent = `${n} selected`;
1674
+
1675
+ if (selectAll) {
1676
+ const total = document.querySelectorAll("#continue-projects tbody .projects-table__checkbox").length;
1677
+ selectAll.checked = n > 0 && n === total;
1678
+ selectAll.indeterminate = n > 0 && n < total;
1679
+ }
1680
+ }
1681
+
1682
+ function bulkDeleteProjects() {
1683
+ if (_bulkSelected.size === 0) return;
1684
+ const names = [..._bulkSelected];
1685
+ const projects = _allProjects.filter((p) => names.includes(p.name));
1686
+
1687
+ const overlay = document.createElement("div");
1688
+ overlay.className = "confirm-overlay";
1689
+ overlay.innerHTML = `
1690
+ <div class="confirm-dialog">
1691
+ <div class="confirm-dialog__title">Delete ${projects.length} project${projects.length > 1 ? "s" : ""}?</div>
1692
+ <div class="confirm-dialog__detail">${projects.map((p) => `<strong>${esc(p.name)}</strong>`).join(", ")}</div>
1693
+ <label class="confirm-dialog__check">
1694
+ <input type="checkbox" id="confirm-bulk-delete-files" checked />
1695
+ <span>Also delete local files</span>
1696
+ </label>
1697
+ <p class="confirm-dialog__warn">Deleting local files cannot be undone.</p>
1698
+ <div class="confirm-dialog__actions">
1699
+ <button class="btn btn--secondary" id="confirm-bulk-cancel">Cancel</button>
1700
+ <button class="btn btn--danger" id="confirm-bulk-delete">Delete</button>
1701
+ </div>
1702
+ </div>
1703
+ `;
1704
+ document.body.appendChild(overlay);
1705
+
1706
+ document.getElementById("confirm-bulk-cancel").addEventListener("click", () => overlay.remove());
1707
+ overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
1708
+
1709
+ document.getElementById("confirm-bulk-delete").addEventListener("click", async () => {
1710
+ const deleteFiles = document.getElementById("confirm-bulk-delete-files").checked;
1711
+ overlay.remove();
1712
+
1713
+ for (const p of projects) {
1714
+ try {
1715
+ if (p.sessionId) {
1716
+ await fetch("/api/themes", {
1717
+ method: "DELETE",
1718
+ headers: { "Content-Type": "application/json" },
1719
+ body: JSON.stringify({ sessionId: p.sessionId, deleteFiles }),
1720
+ });
1721
+ } else if (deleteFiles) {
1722
+ await fetch("/api/themes/delete-local", {
1723
+ method: "POST",
1724
+ headers: { "Content-Type": "application/json" },
1725
+ body: JSON.stringify({ themeName: p.name }),
1726
+ });
1727
+ }
1728
+ } catch { /* continue deleting others */ }
1729
+ }
1730
+ _bulkSelected.clear();
1731
+ await initSetup();
1732
+ populateContinuePanel();
1733
+ });
1734
+ }
1735
+
1736
+ async function bulkDuplicateProjects() {
1737
+ if (_bulkSelected.size === 0) return;
1738
+ const names = [..._bulkSelected];
1739
+ const projects = _allProjects.filter((p) => names.includes(p.name) && p.sessionId);
1740
+
1741
+ if (projects.length === 0) {
1742
+ showError("Only saved projects can be duplicated.");
1743
+ return;
1744
+ }
1745
+
1746
+ for (const p of projects) {
1747
+ try {
1748
+ await fetch("/api/themes/duplicate", {
1749
+ method: "POST",
1750
+ headers: { "Content-Type": "application/json" },
1751
+ body: JSON.stringify({ sessionId: p.sessionId }),
1752
+ });
1753
+ } catch { /* continue */ }
1754
+ }
1755
+ _bulkSelected.clear();
1756
+ await initSetup();
1757
+ populateContinuePanel();
1171
1758
  }
1172
1759
 
1173
1760
  async function loadDownloadPanel() {
@@ -1224,7 +1811,7 @@ function initDlAccountSwitch(accounts, activeId) {
1224
1811
  let html = '<div style="display:flex;flex-direction:column;gap:6px">';
1225
1812
  for (const acct of accounts) {
1226
1813
  const isActive = acct.portalId === activeId;
1227
- html += `<button class="btn btn--${isActive ? "primary" : "secondary"} dl-acct-btn" data-portal="${esc(acct.portalId)}" style="text-align:left;padding:6px 12px;font-size:13px">${esc(acct.portalName || acct.portalId)} (${esc(acct.portalId)})${isActive ? " " : ""}</button>`;
1814
+ html += `<button class="btn btn--${isActive ? "primary" : "secondary"} dl-acct-btn" data-portal="${esc(acct.portalId)}" style="text-align:left;padding:6px 12px;font-size:13px">${esc(acct.portalName || acct.portalId)} (${esc(acct.portalId)})${isActive ? ' <span class="vs-icon-inline">' + vsIcon("check", {size: "sm"}) + '</span>' : ""}</button>`;
1228
1815
  }
1229
1816
  html += `<button class="btn btn--secondary dl-acct-btn" data-portal="__new" style="text-align:left;padding:6px 12px;font-size:13px">+ Add another account</button>`;
1230
1817
  html += '</div>';
@@ -1279,33 +1866,59 @@ async function downloadThemeByName() {
1279
1866
  // Event listeners
1280
1867
  // ---------------------------------------------------------------------------
1281
1868
 
1282
- // Action buttons (advanced "More ways to start" panel)
1283
- document.querySelectorAll(".setup__action-btn").forEach((btn) => {
1284
- btn.addEventListener("click", () => togglePanel(btn.dataset.action));
1869
+ // Asset-type cards (guided entry VIB-255). The "From Template" card opens
1870
+ // the existing starter grid; "Import" shows source picker; others reveal a
1871
+ // pre-scoped describe prompt.
1872
+ document.querySelectorAll(".setup__type-card").forEach((card) => {
1873
+ card.addEventListener("click", () => {
1874
+ const action = card.dataset.action;
1875
+ if (action === "starter") {
1876
+ activePanel = null;
1877
+ togglePanel("starter");
1878
+ setTimeout(() => {
1879
+ document.getElementById("panel-starter")?.scrollIntoView({ behavior: "smooth", block: "nearest" });
1880
+ }, 60);
1881
+ return;
1882
+ }
1883
+ const assetType = card.dataset.assetType;
1884
+ if (assetType === "import") {
1885
+ showImportSources();
1886
+ return;
1887
+ }
1888
+ showScopedPrompt(card);
1889
+ });
1285
1890
  });
1286
1891
 
1287
- // Secondary "Start from Template" button always opens, never toggles closed
1288
- document.querySelectorAll(".setup__secondary-btn").forEach((btn) => {
1289
- btn.addEventListener("click", () => {
1290
- activePanel = null;
1291
- togglePanel(btn.dataset.action);
1292
- setTimeout(() => {
1293
- document.getElementById("panel-starter")?.scrollIntoView({ behavior: "smooth", block: "nearest" });
1294
- }, 60);
1295
- });
1892
+ // "Back" link inside the scoped describe prompt restores the asset-type cards.
1893
+ document.getElementById("setup-prompt-back")?.addEventListener("click", () => {
1894
+ showAssetTypeCards();
1296
1895
  });
1297
1896
 
1298
- // "More ways to start" toggle
1299
- function expandMoreOptions(expand) {
1300
- const toggle = document.getElementById("setup-more-toggle");
1301
- const panel = document.getElementById("setup-more-panel");
1302
- if (!toggle || !panel) return;
1303
- const willExpand = expand ?? panel.classList.contains("hidden");
1304
- panel.classList.toggle("hidden", !willExpand);
1305
- toggle.setAttribute("aria-expanded", willExpand ? "true" : "false");
1306
- toggle.classList.toggle("setup__more-toggle--open", willExpand);
1897
+ // Import source picker
1898
+ function showImportSources() {
1899
+ const cards = document.getElementById("setup-type-cards");
1900
+ const importPanel = document.getElementById("setup-import-sources");
1901
+ const question = document.getElementById("setup-question");
1902
+ if (cards) cards.classList.add("hidden");
1903
+ if (question) question.classList.add("hidden");
1904
+ if (importPanel) importPanel.classList.remove("hidden");
1307
1905
  }
1308
- document.getElementById("setup-more-toggle")?.addEventListener("click", () => expandMoreOptions());
1906
+
1907
+ document.getElementById("setup-import-back")?.addEventListener("click", () => {
1908
+ const importPanel = document.getElementById("setup-import-sources");
1909
+ if (importPanel) importPanel.classList.add("hidden");
1910
+ showAssetTypeCards();
1911
+ });
1912
+
1913
+ document.querySelectorAll(".setup__import-btn").forEach((btn) => {
1914
+ btn.addEventListener("click", () => {
1915
+ const action = btn.dataset.action;
1916
+ const importPanel = document.getElementById("setup-import-sources");
1917
+ if (importPanel) importPanel.classList.add("hidden");
1918
+ showAssetTypeCards();
1919
+ togglePanel(action);
1920
+ });
1921
+ });
1309
1922
 
1310
1923
  // "View all" link in recent projects → open the full Continue panel
1311
1924
  document.getElementById("setup-recent-all")?.addEventListener("click", () => {
@@ -1739,8 +2352,7 @@ function handleRoute() {
1739
2352
  if (appTemplateMatch) {
1740
2353
  const themeName = decodeURIComponent(appTemplateMatch[1]);
1741
2354
  const templateId = decodeURIComponent(appTemplateMatch[2]);
1742
- // Already showing this nothing to do
1743
- if (currentAppTheme === themeName && !appScreen.classList.contains("hidden")) return;
2355
+ if (currentAppTheme === themeName && appBody.dataset.mode === "editor") return;
1744
2356
  // Open theme then activate template
1745
2357
  openTheme(themeName).then(() => {
1746
2358
  if (typeof showChat === "function") {
@@ -1754,24 +2366,22 @@ function handleRoute() {
1754
2366
  const appMatch = hash.match(/^#\/app\/([^/]+)$/);
1755
2367
  if (appMatch) {
1756
2368
  const themeName = decodeURIComponent(appMatch[1]);
1757
- if (currentAppTheme === themeName && !appScreen.classList.contains("hidden")) return;
2369
+ if (currentAppTheme === themeName && appBody.dataset.mode === "editor") return;
1758
2370
  openTheme(themeName);
1759
2371
  return;
1760
2372
  }
1761
2373
 
1762
- // #/dashboard/{themeName} → show dashboard for theme
2374
+ // #/dashboard/{themeName} → show editor for theme
1763
2375
  const dashMatch = hash.match(/^#\/dashboard\/(.+)$/);
1764
2376
  if (dashMatch) {
1765
2377
  const themeName = decodeURIComponent(dashMatch[1]);
1766
- const dashEl = document.getElementById("dashboard-screen");
1767
- if (currentDashboardTheme === themeName && dashEl && !dashEl.classList.contains("hidden")) return;
2378
+ if (currentDashboardTheme === themeName && appBody.dataset.mode === "editor") return;
1768
2379
  openTheme(themeName);
1769
2380
  return;
1770
2381
  }
1771
2382
 
1772
2383
  // Default: show setup
1773
- const dashEl = document.getElementById("dashboard-screen");
1774
- if (!appScreen.classList.contains("hidden") || (dashEl && !dashEl.classList.contains("hidden"))) {
2384
+ if (appBody.dataset.mode === "editor") {
1775
2385
  showSetup();
1776
2386
  }
1777
2387
  }