vibespot 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +103 -33
- package/README.md +55 -6
- package/assets/blog-rules.md +251 -0
- package/assets/email-rules.md +390 -0
- package/assets/humanify-guide.md +300 -101
- package/assets/plan-templates/agency-services.md +42 -0
- package/assets/plan-templates/blog-content-hub.md +50 -0
- package/assets/plan-templates/ecommerce-product.md +42 -0
- package/assets/plan-templates/email-announcement.md +41 -0
- package/assets/plan-templates/email-event-invite.md +43 -0
- package/assets/plan-templates/email-newsletter.md +41 -0
- package/assets/plan-templates/email-re-engagement.md +42 -0
- package/assets/plan-templates/email-welcome.md +41 -0
- package/assets/plan-templates/event-registration.md +42 -0
- package/assets/plan-templates/portfolio.md +41 -0
- package/assets/plan-templates/restaurant.md +42 -0
- package/assets/plan-templates/saas-landing.md +42 -0
- package/dist/index.js +1485 -397
- package/dist/index.js.map +1 -1
- package/package.json +11 -7
- package/starters/01-saas-landing.json +43 -0
- package/starters/02-portfolio.json +39 -0
- package/starters/03-restaurant.json +39 -0
- package/starters/04-event.json +39 -0
- package/starters/05-coming-soon.json +32 -0
- package/starters/06-blog-content-hub.json +75 -0
- package/starters/06-email-welcome.json +60 -0
- package/starters/07-email-announcement.json +60 -0
- package/starters/08-email-newsletter.json +52 -0
- package/ui/chat.js +1604 -155
- package/ui/code-editor.js +49 -7
- package/ui/dashboard.js +551 -83
- package/ui/docs/docs.css +29 -0
- package/ui/docs/index.html +274 -117
- package/ui/docs/screenshots/brand-kit-preview.png +0 -0
- package/ui/docs/screenshots/content-type-dropdown.png +0 -0
- package/ui/docs/screenshots/editor-full-layout.png +0 -0
- package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
- package/ui/docs/screenshots/multi-page-tree.png +0 -0
- package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
- package/ui/docs/screenshots/split-pane-view.png +0 -0
- package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
- package/ui/docs/screenshots/workspace-tabs.png +0 -0
- package/ui/email-preview.js +109 -0
- package/ui/field-editor.js +71 -0
- package/ui/icons.js +120 -0
- package/ui/index.html +882 -515
- package/ui/inline-edit.js +710 -0
- package/ui/marketplace.js +218 -0
- package/ui/plan.js +0 -0
- package/ui/preview.js +219 -1
- package/ui/section-controls.js +628 -0
- package/ui/settings.js +84 -28
- package/ui/setup.js +1016 -118
- package/ui/styles.css +6119 -2456
- package/ui/upload-panel.js +47 -20
package/ui/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
|
|
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
|
|
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();
|
|
14
34
|
}
|
|
15
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
|
+
}
|
|
66
|
+
}
|
|
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("
|
|
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
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
<div class="project-
|
|
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,9 +130,12 @@ async function initSetup() {
|
|
|
49
130
|
const res = await fetch("/api/setup");
|
|
50
131
|
const info = await res.json();
|
|
51
132
|
|
|
52
|
-
// Populate the project
|
|
133
|
+
// Populate the project switcher with all projects (used in editor mode)
|
|
53
134
|
populateProjectRail(info);
|
|
54
135
|
|
|
136
|
+
// Show "Continue where you left off" cards above the create options
|
|
137
|
+
populateRecentProjects(info);
|
|
138
|
+
|
|
55
139
|
// Auto-select engine if available but not yet chosen
|
|
56
140
|
if (info.availableEngines && info.availableEngines.length > 0 && !info.activeEngine) {
|
|
57
141
|
const engine = info.availableEngines[0];
|
|
@@ -93,22 +177,118 @@ async function initSetup() {
|
|
|
93
177
|
}, 0);
|
|
94
178
|
}
|
|
95
179
|
|
|
96
|
-
//
|
|
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).
|
|
97
191
|
// Add ?walkthrough to URL to force-show it for testing
|
|
98
|
-
if (
|
|
99
|
-
(!info.aiAvailable &&
|
|
192
|
+
if (params.has("walkthrough") ||
|
|
193
|
+
(!info.aiAvailable && isFreshUser)) {
|
|
100
194
|
showWalkthrough();
|
|
101
195
|
return;
|
|
102
196
|
}
|
|
103
197
|
|
|
198
|
+
// Track server content mode (email vs page)
|
|
199
|
+
_serverContentMode = info.contentMode || "page";
|
|
200
|
+
|
|
104
201
|
// Reset panel state
|
|
105
202
|
remoteThemesLoaded = false;
|
|
106
203
|
|
|
204
|
+
// Reset starter cache so each visit re-fetches from server
|
|
205
|
+
_startersCache = null;
|
|
206
|
+
|
|
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).
|
|
211
|
+
activePanel = null;
|
|
212
|
+
showAssetTypeCards();
|
|
213
|
+
|
|
107
214
|
} catch (err) {
|
|
108
215
|
showError("Could not connect to server. Is vibeSpot running?");
|
|
109
216
|
}
|
|
110
217
|
}
|
|
111
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
|
+
|
|
112
292
|
async function saveAlertApiKey(key) {
|
|
113
293
|
if (!key) return;
|
|
114
294
|
// Detect provider from key prefix
|
|
@@ -159,6 +339,9 @@ function deduplicateProjects(info) {
|
|
|
159
339
|
updatedAt: s.updatedAt,
|
|
160
340
|
moduleCount: s.moduleCount ?? null,
|
|
161
341
|
templateCount: s.templateCount ?? null,
|
|
342
|
+
pageCount: s.pageCount ?? 0,
|
|
343
|
+
emailCount: s.emailCount ?? 0,
|
|
344
|
+
hasBrandAssets: s.hasBrandAssets ?? false,
|
|
162
345
|
});
|
|
163
346
|
}
|
|
164
347
|
}
|
|
@@ -174,6 +357,9 @@ function deduplicateProjects(info) {
|
|
|
174
357
|
updatedAt: null,
|
|
175
358
|
moduleCount: typeof t === "object" ? t.moduleCount ?? null : null,
|
|
176
359
|
templateCount: null,
|
|
360
|
+
pageCount: 0,
|
|
361
|
+
emailCount: 0,
|
|
362
|
+
hasBrandAssets: false,
|
|
177
363
|
});
|
|
178
364
|
}
|
|
179
365
|
}
|
|
@@ -182,40 +368,115 @@ function deduplicateProjects(info) {
|
|
|
182
368
|
}
|
|
183
369
|
|
|
184
370
|
// ---------------------------------------------------------------------------
|
|
185
|
-
//
|
|
371
|
+
// "Continue where you left off" — recent projects above the create options
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
const RECENT_PROJECTS_LIMIT = 4;
|
|
375
|
+
let _allProjects = [];
|
|
376
|
+
|
|
377
|
+
function populateRecentProjects(info) {
|
|
378
|
+
const section = document.getElementById("setup-recent");
|
|
379
|
+
const list = document.getElementById("setup-recent-list");
|
|
380
|
+
const viewAll = document.getElementById("setup-recent-all");
|
|
381
|
+
if (!section || !list) return;
|
|
382
|
+
|
|
383
|
+
const projects = deduplicateProjects(info);
|
|
384
|
+
if (projects.length === 0) {
|
|
385
|
+
_allProjects = [];
|
|
386
|
+
section.classList.add("hidden");
|
|
387
|
+
section.dataset.hasItems = "0";
|
|
388
|
+
list.innerHTML = "";
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
section.dataset.hasItems = "1";
|
|
392
|
+
|
|
393
|
+
// Most recently updated first; locals (no updatedAt) follow
|
|
394
|
+
const withTime = projects.filter((p) => p.updatedAt).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
395
|
+
const withoutTime = projects.filter((p) => !p.updatedAt);
|
|
396
|
+
const ordered = [...withTime, ...withoutTime];
|
|
397
|
+
_allProjects = ordered;
|
|
398
|
+
const top = ordered.slice(0, RECENT_PROJECTS_LIMIT);
|
|
399
|
+
|
|
400
|
+
list.innerHTML = "";
|
|
401
|
+
for (const p of top) {
|
|
402
|
+
const card = document.createElement("button");
|
|
403
|
+
card.type = "button";
|
|
404
|
+
card.className = "setup__recent-card";
|
|
405
|
+
|
|
406
|
+
const initial = p.name.charAt(0).toUpperCase();
|
|
407
|
+
const meta = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
|
|
408
|
+
|
|
409
|
+
card.innerHTML =
|
|
410
|
+
`<span class="setup__recent-card-bubble">${esc(initial)}</span>` +
|
|
411
|
+
`<span class="setup__recent-card-text">` +
|
|
412
|
+
`<span class="setup__recent-card-name">${esc(p.name)}</span>` +
|
|
413
|
+
`<span class="setup__recent-card-meta">${esc(meta)}</span>` +
|
|
414
|
+
`</span>`;
|
|
415
|
+
|
|
416
|
+
const delBtn = document.createElement("button");
|
|
417
|
+
delBtn.type = "button";
|
|
418
|
+
delBtn.className = "setup__recent-card-delete";
|
|
419
|
+
delBtn.innerHTML = "×";
|
|
420
|
+
delBtn.title = "Delete project";
|
|
421
|
+
delBtn.addEventListener("click", (e) => {
|
|
422
|
+
e.stopPropagation();
|
|
423
|
+
confirmDeleteProject(p);
|
|
424
|
+
});
|
|
425
|
+
card.appendChild(delBtn);
|
|
426
|
+
|
|
427
|
+
card.addEventListener("click", () => {
|
|
428
|
+
if (typeof isStreaming !== "undefined" && isStreaming) {
|
|
429
|
+
showError("Cannot switch projects while AI is generating.");
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (p.sessionId) resumeSession(p.sessionId);
|
|
433
|
+
else openTheme(p.name);
|
|
434
|
+
});
|
|
435
|
+
list.appendChild(card);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (viewAll) viewAll.classList.toggle("hidden", projects.length <= top.length);
|
|
439
|
+
section.classList.remove("hidden");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
// Project switcher (rendered into the editor-mode rail popover)
|
|
186
444
|
// ---------------------------------------------------------------------------
|
|
187
445
|
|
|
188
446
|
const railTooltip = document.getElementById("project-rail-tooltip");
|
|
189
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
|
+
*/
|
|
190
453
|
function populateProjectRail(info) {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
rail.innerHTML = "";
|
|
454
|
+
const list = document.getElementById("project-switcher-list");
|
|
455
|
+
if (!list) return;
|
|
456
|
+
list.innerHTML = "";
|
|
195
457
|
|
|
196
458
|
const projects = deduplicateProjects(info);
|
|
197
|
-
if (countEl) countEl.textContent = projects.length;
|
|
198
459
|
|
|
199
460
|
if (projects.length === 0) {
|
|
200
|
-
|
|
461
|
+
list.innerHTML = '<div class="project-switcher__empty">No projects yet.<br>Create one to get started.</div>';
|
|
201
462
|
return;
|
|
202
463
|
}
|
|
203
464
|
|
|
204
465
|
for (const p of projects) {
|
|
205
|
-
const item = document.createElement("
|
|
466
|
+
const item = document.createElement("button");
|
|
467
|
+
item.type = "button";
|
|
206
468
|
item.className = "project-rail__item";
|
|
207
469
|
item.dataset.name = p.name;
|
|
470
|
+
if (p.sessionId) item.dataset.sessionId = p.sessionId;
|
|
208
471
|
|
|
209
472
|
const initial = p.name.charAt(0).toUpperCase();
|
|
210
473
|
const meta = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
|
|
211
474
|
|
|
212
|
-
// Bubble (always visible — in collapsed mode this is the only thing shown)
|
|
213
475
|
const bubble = document.createElement("div");
|
|
214
476
|
bubble.className = "project-rail__item-bubble";
|
|
215
477
|
bubble.textContent = initial;
|
|
216
478
|
item.appendChild(bubble);
|
|
217
479
|
|
|
218
|
-
// Info (visible when expanded via CSS)
|
|
219
480
|
const infoEl = document.createElement("div");
|
|
220
481
|
infoEl.className = "project-rail__item-info";
|
|
221
482
|
infoEl.innerHTML = `
|
|
@@ -223,7 +484,6 @@ function populateProjectRail(info) {
|
|
|
223
484
|
<span class="project-rail__item-meta">${meta}</span>`;
|
|
224
485
|
item.appendChild(infoEl);
|
|
225
486
|
|
|
226
|
-
// Double-click on name to rename
|
|
227
487
|
const nameSpan = infoEl.querySelector(".project-rail__item-name");
|
|
228
488
|
if (nameSpan) {
|
|
229
489
|
nameSpan.addEventListener("dblclick", (e) => {
|
|
@@ -232,8 +492,8 @@ function populateProjectRail(info) {
|
|
|
232
492
|
});
|
|
233
493
|
}
|
|
234
494
|
|
|
235
|
-
// Delete button (visible when expanded + hover)
|
|
236
495
|
const delBtn = document.createElement("button");
|
|
496
|
+
delBtn.type = "button";
|
|
237
497
|
delBtn.className = "project-rail__item-delete";
|
|
238
498
|
delBtn.innerHTML = "×";
|
|
239
499
|
delBtn.title = "Delete project";
|
|
@@ -243,44 +503,17 @@ function populateProjectRail(info) {
|
|
|
243
503
|
});
|
|
244
504
|
item.appendChild(delBtn);
|
|
245
505
|
|
|
246
|
-
// Tooltip (only when collapsed — skip when expanded since name is visible)
|
|
247
|
-
item.addEventListener("mouseenter", () => {
|
|
248
|
-
const railEl = document.getElementById("project-rail");
|
|
249
|
-
if (railEl && railEl.classList.contains("project-rail--expanded")) return;
|
|
250
|
-
|
|
251
|
-
let stats = "";
|
|
252
|
-
if (p.moduleCount != null) {
|
|
253
|
-
stats = p.moduleCount + " module" + (p.moduleCount !== 1 ? "s" : "");
|
|
254
|
-
if (p.templateCount > 1) stats += " \u00b7 " + p.templateCount + " templates";
|
|
255
|
-
stats += p.updatedAt ? " \u00b7 " + timeAgo(p.updatedAt) : " \u00b7 on disk";
|
|
256
|
-
} else {
|
|
257
|
-
stats = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
railTooltip.innerHTML =
|
|
261
|
-
'<div class="project-rail__tooltip-name">' + esc(p.name) + "</div>" +
|
|
262
|
-
'<div class="project-rail__tooltip-stats">' + stats + "</div>";
|
|
263
|
-
|
|
264
|
-
const rect = item.getBoundingClientRect();
|
|
265
|
-
railTooltip.style.top = rect.top + "px";
|
|
266
|
-
railTooltip.classList.add("project-rail__tooltip--visible");
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
item.addEventListener("mouseleave", () => {
|
|
270
|
-
railTooltip.classList.remove("project-rail__tooltip--visible");
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// Click to open (blocked while AI is generating)
|
|
274
506
|
item.addEventListener("click", () => {
|
|
275
507
|
if (typeof isStreaming !== "undefined" && isStreaming) {
|
|
276
508
|
showError("Cannot switch projects while AI is generating.");
|
|
277
509
|
return;
|
|
278
510
|
}
|
|
511
|
+
closeProjectSwitcher();
|
|
279
512
|
if (p.sessionId) resumeSession(p.sessionId);
|
|
280
513
|
else openTheme(p.name);
|
|
281
514
|
});
|
|
282
515
|
|
|
283
|
-
|
|
516
|
+
list.appendChild(item);
|
|
284
517
|
}
|
|
285
518
|
|
|
286
519
|
updateRailActive();
|
|
@@ -291,14 +524,99 @@ function updateRailActive() {
|
|
|
291
524
|
document.querySelectorAll(".project-rail__item").forEach((btn) => {
|
|
292
525
|
btn.classList.toggle("project-rail__item--active", btn.dataset.name === current);
|
|
293
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(() => {});
|
|
294
553
|
}
|
|
295
554
|
|
|
296
|
-
|
|
297
|
-
document.getElementById("project-
|
|
298
|
-
|
|
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();
|
|
567
|
+
}
|
|
568
|
+
|
|
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();
|
|
299
586
|
togglePanel("new");
|
|
300
587
|
});
|
|
301
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
|
+
|
|
302
620
|
// ---------------------------------------------------------------------------
|
|
303
621
|
// Inline rename
|
|
304
622
|
// ---------------------------------------------------------------------------
|
|
@@ -431,6 +749,146 @@ function confirmDeleteProject(project) {
|
|
|
431
749
|
});
|
|
432
750
|
}
|
|
433
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 — 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> → HubSpot <strong>modules</strong> with editable fields</li>
|
|
818
|
+
<li><strong>Shared CSS & tokens</strong> → theme-level <code>:root</code> variables</li>
|
|
819
|
+
<li><strong>Project</strong> → uploadable <strong>HubSpot CMS theme</strong> with templates</li>
|
|
820
|
+
</ul>
|
|
821
|
+
<p>Marketers can keep editing fields in HubSpot after upload — no code changes required.</p>
|
|
822
|
+
`,
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
title: "Try it with a pre-filled prompt",
|
|
826
|
+
body: `
|
|
827
|
+
<p>We’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
|
+
|
|
434
892
|
// ---------------------------------------------------------------------------
|
|
435
893
|
// Guided walkthrough (first-run experience)
|
|
436
894
|
// ---------------------------------------------------------------------------
|
|
@@ -664,8 +1122,191 @@ async function createTheme() {
|
|
|
664
1122
|
}
|
|
665
1123
|
}
|
|
666
1124
|
|
|
1125
|
+
// ---------------------------------------------------------------------------
|
|
1126
|
+
// Primary path: "Describe the landing page you want to build..."
|
|
1127
|
+
// Creates a fresh theme and forwards the prompt to the chat once it connects.
|
|
1128
|
+
// ---------------------------------------------------------------------------
|
|
1129
|
+
|
|
1130
|
+
function generateThemeNameFromPrompt(prompt) {
|
|
1131
|
+
const slug = prompt
|
|
1132
|
+
.toLowerCase()
|
|
1133
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
1134
|
+
.trim()
|
|
1135
|
+
.split(/\s+/)
|
|
1136
|
+
.slice(0, 5)
|
|
1137
|
+
.join("-")
|
|
1138
|
+
.replace(/-+/g, "-")
|
|
1139
|
+
.replace(/^-|-$/g, "")
|
|
1140
|
+
.slice(0, 40);
|
|
1141
|
+
|
|
1142
|
+
if (slug) return slug;
|
|
1143
|
+
return "page-" + Date.now().toString(36);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async function startFromPrompt() {
|
|
1147
|
+
const input = document.getElementById("setup-prompt-input");
|
|
1148
|
+
const submitBtn = document.getElementById("setup-prompt-submit");
|
|
1149
|
+
const prompt = (input?.value || "").trim();
|
|
1150
|
+
if (!prompt) {
|
|
1151
|
+
input?.focus();
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
1156
|
+
const themeName = generateThemeNameFromPrompt(prompt);
|
|
1157
|
+
showLoading("Creating theme...");
|
|
1158
|
+
|
|
1159
|
+
try {
|
|
1160
|
+
const createBody = { name: themeName };
|
|
1161
|
+
if (window.__pendingAssetType) createBody.assetType = window.__pendingAssetType;
|
|
1162
|
+
const res = await fetch("/api/setup/create", {
|
|
1163
|
+
method: "POST",
|
|
1164
|
+
headers: { "Content-Type": "application/json" },
|
|
1165
|
+
body: JSON.stringify(createBody),
|
|
1166
|
+
});
|
|
1167
|
+
const data = await res.json();
|
|
1168
|
+
|
|
1169
|
+
if (data.error) {
|
|
1170
|
+
showError(data.error);
|
|
1171
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// chat.js will pick this up on the next ws "init" message
|
|
1176
|
+
window.__pendingInitialPrompt = prompt;
|
|
1177
|
+
if (input) input.value = "";
|
|
1178
|
+
showAppDirect(data.themeName);
|
|
1179
|
+
} catch (err) {
|
|
1180
|
+
showError("Failed to create theme: " + err.message);
|
|
1181
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// ---------------------------------------------------------------------------
|
|
1186
|
+
// Starter templates
|
|
1187
|
+
// ---------------------------------------------------------------------------
|
|
1188
|
+
|
|
1189
|
+
let _startersCache = null;
|
|
1190
|
+
let _selectedStarterId = null;
|
|
1191
|
+
|
|
1192
|
+
function escHtml(s) {
|
|
1193
|
+
const d = document.createElement("div");
|
|
1194
|
+
d.textContent = s;
|
|
1195
|
+
return d.innerHTML;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
async function loadStarterGrid() {
|
|
1199
|
+
const grid = document.getElementById("starter-grid");
|
|
1200
|
+
if (!grid) return;
|
|
1201
|
+
|
|
1202
|
+
if (_startersCache !== null) {
|
|
1203
|
+
renderStarterGrid(_startersCache);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
try {
|
|
1208
|
+
const res = await fetch("/api/starters");
|
|
1209
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1210
|
+
const data = await res.json();
|
|
1211
|
+
_startersCache = data.starters || [];
|
|
1212
|
+
renderStarterGrid(_startersCache);
|
|
1213
|
+
} catch {
|
|
1214
|
+
// API unavailable — attach click listeners to any hardcoded static cards
|
|
1215
|
+
grid.querySelectorAll(".starter-card").forEach((card) => {
|
|
1216
|
+
card.addEventListener("click", () => selectStarter(card.dataset.starterId));
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function renderStarterGrid(starters) {
|
|
1222
|
+
const grid = document.getElementById("starter-grid");
|
|
1223
|
+
if (!grid) return;
|
|
1224
|
+
|
|
1225
|
+
if (starters.length === 0) {
|
|
1226
|
+
grid.innerHTML = '<p class="setup__hint">No starter templates available.</p>';
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
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) => `
|
|
1234
|
+
<div class="starter-card${_selectedStarterId === s.id ? " selected" : ""}" data-starter-id="${escHtml(s.id)}">
|
|
1235
|
+
<span class="starter-card__name">${escHtml(s.name)}</span>
|
|
1236
|
+
<span class="starter-card__desc">${escHtml(s.description)}</span>
|
|
1237
|
+
<span class="starter-card__meta">${s.moduleCount} modules</span>
|
|
1238
|
+
</div>
|
|
1239
|
+
`).join("");
|
|
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
|
+
|
|
1257
|
+
grid.querySelectorAll(".starter-card").forEach((card) => {
|
|
1258
|
+
card.addEventListener("click", () => selectStarter(card.dataset.starterId));
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function selectStarter(id) {
|
|
1263
|
+
_selectedStarterId = id;
|
|
1264
|
+
const starter = (_startersCache || []).find((s) => s.id === id);
|
|
1265
|
+
if (!starter) return;
|
|
1266
|
+
|
|
1267
|
+
document.querySelectorAll(".starter-card").forEach((c) => c.classList.toggle("selected", c.dataset.starterId === id));
|
|
1268
|
+
|
|
1269
|
+
const createSection = document.getElementById("starter-create");
|
|
1270
|
+
const label = document.getElementById("starter-create-label");
|
|
1271
|
+
if (createSection && label) {
|
|
1272
|
+
label.textContent = `Create theme from "${starter.name}":`;
|
|
1273
|
+
createSection.classList.remove("hidden");
|
|
1274
|
+
setTimeout(() => document.getElementById("starter-theme-name")?.focus(), 50);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async function createFromStarter() {
|
|
1279
|
+
if (!_selectedStarterId) return;
|
|
1280
|
+
const name = document.getElementById("starter-theme-name").value.trim();
|
|
1281
|
+
if (!name) {
|
|
1282
|
+
showError("Please enter a name for your theme.");
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
showLoading("Creating theme from template...");
|
|
1287
|
+
|
|
1288
|
+
try {
|
|
1289
|
+
const res = await fetch("/api/setup/create", {
|
|
1290
|
+
method: "POST",
|
|
1291
|
+
headers: { "Content-Type": "application/json" },
|
|
1292
|
+
body: JSON.stringify({ name, starterId: _selectedStarterId }),
|
|
1293
|
+
});
|
|
1294
|
+
const data = await res.json();
|
|
1295
|
+
|
|
1296
|
+
if (data.error) {
|
|
1297
|
+
showError(data.error);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
showApp(data.themeName);
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
showError("Failed to create theme: " + err.message);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
667
1307
|
async function fetchTheme() {
|
|
668
|
-
const
|
|
1308
|
+
const nameEl = document.getElementById("fetch-theme-name") || document.getElementById("dl-theme-name");
|
|
1309
|
+
const name = nameEl ? nameEl.value.trim() : "";
|
|
669
1310
|
if (!name) {
|
|
670
1311
|
showError("Please enter the theme name from your HubSpot account.");
|
|
671
1312
|
return;
|
|
@@ -770,16 +1411,12 @@ function showApp(themeName) {
|
|
|
770
1411
|
* Used as fallback or when navigating from dashboard to a specific template.
|
|
771
1412
|
*/
|
|
772
1413
|
function showAppDirect(themeName) {
|
|
773
|
-
setupScreen.classList.add("hidden");
|
|
774
|
-
document.getElementById("setup-topbar").classList.add("hidden");
|
|
775
|
-
document.getElementById("project-rail")?.classList.remove("project-rail--expanded");
|
|
776
1414
|
if (typeof hideDashboard === "function") hideDashboard();
|
|
1415
|
+
appBody.dataset.mode = "editor";
|
|
777
1416
|
appScreen.classList.remove("hidden");
|
|
1417
|
+
document.getElementById("project-rail")?.setAttribute("data-mode", "editor");
|
|
778
1418
|
document.getElementById("theme-name").textContent = themeName;
|
|
779
1419
|
|
|
780
|
-
const urlEl = document.getElementById("browser-url");
|
|
781
|
-
if (urlEl) urlEl.textContent = themeName + ".vibespot.app";
|
|
782
|
-
|
|
783
1420
|
currentAppTheme = themeName;
|
|
784
1421
|
const target = "#/app/" + encodeURIComponent(themeName);
|
|
785
1422
|
if (location.hash !== target) {
|
|
@@ -798,9 +1435,9 @@ function showAppDirect(themeName) {
|
|
|
798
1435
|
function showSetup() {
|
|
799
1436
|
appScreen.classList.add("hidden");
|
|
800
1437
|
if (typeof hideDashboard === "function") hideDashboard();
|
|
801
|
-
|
|
802
|
-
document.getElementById("
|
|
803
|
-
|
|
1438
|
+
appBody.dataset.mode = "project-home";
|
|
1439
|
+
document.getElementById("project-rail")?.setAttribute("data-mode", "project-home");
|
|
1440
|
+
closeProjectSwitcher();
|
|
804
1441
|
currentAppTheme = "";
|
|
805
1442
|
|
|
806
1443
|
hideLoading();
|
|
@@ -813,25 +1450,16 @@ function showSetup() {
|
|
|
813
1450
|
initSetup();
|
|
814
1451
|
}
|
|
815
1452
|
|
|
816
|
-
//
|
|
817
|
-
document.getElementById("
|
|
818
|
-
|
|
819
|
-
appScreen.classList.add("hidden");
|
|
820
|
-
showDashboard(currentAppTheme);
|
|
821
|
-
}
|
|
1453
|
+
// Editor back button → go back to setup
|
|
1454
|
+
document.getElementById("editor-back")?.addEventListener("click", () => {
|
|
1455
|
+
showSetup();
|
|
822
1456
|
});
|
|
823
1457
|
|
|
824
|
-
// Logo click → go back to setup
|
|
1458
|
+
// Logo click → go back to setup
|
|
825
1459
|
document.querySelectorAll(".topbar__brand").forEach((el) => {
|
|
826
1460
|
el.style.cursor = "pointer";
|
|
827
1461
|
el.addEventListener("click", () => {
|
|
828
|
-
|
|
829
|
-
if (dashEl && !dashEl.classList.contains("hidden")) {
|
|
830
|
-
showSetup();
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
// Fallback
|
|
834
|
-
if (!appScreen.classList.contains("hidden")) {
|
|
1462
|
+
if (appBody.dataset.mode === "editor") {
|
|
835
1463
|
showSetup();
|
|
836
1464
|
}
|
|
837
1465
|
});
|
|
@@ -870,7 +1498,7 @@ let remoteThemesLoaded = false;
|
|
|
870
1498
|
|
|
871
1499
|
function togglePanel(action) {
|
|
872
1500
|
const panels = document.querySelectorAll(".setup__panel");
|
|
873
|
-
const buttons = document.querySelectorAll(".
|
|
1501
|
+
const buttons = document.querySelectorAll(".setup__entry-card");
|
|
874
1502
|
|
|
875
1503
|
// Close if same panel clicked
|
|
876
1504
|
if (activePanel === action) {
|
|
@@ -884,18 +1512,19 @@ function togglePanel(action) {
|
|
|
884
1512
|
panels.forEach((p) => p.classList.add("hidden"));
|
|
885
1513
|
buttons.forEach((b) => b.classList.remove("active"));
|
|
886
1514
|
|
|
887
|
-
const panelMap = { new: "panel-new", continue: "panel-continue", download: "panel-download", figma: "panel-figma", convert: "panel-convert" };
|
|
1515
|
+
const panelMap = { starter: "panel-starter", new: "panel-new", continue: "panel-continue", download: "panel-download", figma: "panel-figma", convert: "panel-convert" };
|
|
888
1516
|
const panel = document.getElementById(panelMap[action]);
|
|
889
1517
|
if (panel) {
|
|
890
1518
|
panel.classList.remove("hidden");
|
|
891
1519
|
activePanel = action;
|
|
892
1520
|
}
|
|
893
1521
|
|
|
894
|
-
// Mark
|
|
895
|
-
const btn = document.querySelector(`.
|
|
1522
|
+
// Mark card active
|
|
1523
|
+
const btn = document.querySelector(`.setup__entry-card[data-action="${action}"]`);
|
|
896
1524
|
if (btn) btn.classList.add("active");
|
|
897
1525
|
|
|
898
1526
|
// Focus input if applicable
|
|
1527
|
+
if (action === "starter") loadStarterGrid();
|
|
899
1528
|
if (action === "new") setTimeout(() => document.getElementById("new-theme-name")?.focus(), 50);
|
|
900
1529
|
if (action === "convert") setTimeout(() => document.getElementById("import-url")?.focus(), 50);
|
|
901
1530
|
if (action === "figma") initFigmaPanel();
|
|
@@ -907,39 +1536,225 @@ function togglePanel(action) {
|
|
|
907
1536
|
if (action === "continue") populateContinuePanel();
|
|
908
1537
|
}
|
|
909
1538
|
|
|
1539
|
+
let _bulkSelected = new Set();
|
|
1540
|
+
|
|
910
1541
|
function populateContinuePanel() {
|
|
911
1542
|
const container = document.getElementById("continue-projects");
|
|
912
1543
|
const empty = document.getElementById("continue-empty");
|
|
913
1544
|
if (!container) return;
|
|
914
1545
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (railItems.length === 0) {
|
|
1546
|
+
const projects = _allProjects;
|
|
1547
|
+
if (!projects || projects.length === 0) {
|
|
918
1548
|
container.innerHTML = "";
|
|
919
|
-
|
|
1549
|
+
_bulkSelected.clear();
|
|
1550
|
+
if (empty) empty.classList.remove("hidden");
|
|
920
1551
|
return;
|
|
921
1552
|
}
|
|
922
1553
|
|
|
923
|
-
empty.classList.add("hidden");
|
|
924
|
-
|
|
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
|
+
}
|
|
925
1607
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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);
|
|
1615
|
+
|
|
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;
|
|
939
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);
|
|
1636
|
+
});
|
|
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);
|
|
940
1657
|
});
|
|
941
|
-
|
|
1658
|
+
syncBulkToolbar();
|
|
942
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();
|
|
943
1758
|
}
|
|
944
1759
|
|
|
945
1760
|
async function loadDownloadPanel() {
|
|
@@ -996,7 +1811,7 @@ function initDlAccountSwitch(accounts, activeId) {
|
|
|
996
1811
|
let html = '<div style="display:flex;flex-direction:column;gap:6px">';
|
|
997
1812
|
for (const acct of accounts) {
|
|
998
1813
|
const isActive = acct.portalId === activeId;
|
|
999
|
-
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 ? "
|
|
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>`;
|
|
1000
1815
|
}
|
|
1001
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>`;
|
|
1002
1817
|
html += '</div>';
|
|
@@ -1051,9 +1866,95 @@ async function downloadThemeByName() {
|
|
|
1051
1866
|
// Event listeners
|
|
1052
1867
|
// ---------------------------------------------------------------------------
|
|
1053
1868
|
|
|
1054
|
-
//
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
+
});
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
// "Back" link inside the scoped describe prompt restores the asset-type cards.
|
|
1893
|
+
document.getElementById("setup-prompt-back")?.addEventListener("click", () => {
|
|
1894
|
+
showAssetTypeCards();
|
|
1895
|
+
});
|
|
1896
|
+
|
|
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");
|
|
1905
|
+
}
|
|
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
|
+
});
|
|
1922
|
+
|
|
1923
|
+
// "View all" link in recent projects → open the full Continue panel
|
|
1924
|
+
document.getElementById("setup-recent-all")?.addEventListener("click", () => {
|
|
1925
|
+
togglePanel("continue");
|
|
1926
|
+
setTimeout(() => {
|
|
1927
|
+
document.getElementById("panel-continue")?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1928
|
+
}, 60);
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
// Primary "describe-it" prompt
|
|
1932
|
+
const promptInputEl = document.getElementById("setup-prompt-input");
|
|
1933
|
+
const promptSubmitEl = document.getElementById("setup-prompt-submit");
|
|
1934
|
+
if (promptInputEl && promptSubmitEl) {
|
|
1935
|
+
const syncSubmitState = () => {
|
|
1936
|
+
promptSubmitEl.disabled = promptInputEl.value.trim().length === 0;
|
|
1937
|
+
};
|
|
1938
|
+
promptInputEl.addEventListener("input", syncSubmitState);
|
|
1939
|
+
promptInputEl.addEventListener("keydown", (e) => {
|
|
1940
|
+
// ⌘/Ctrl + Enter submits; plain Enter inserts newline like a normal textarea.
|
|
1941
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
1942
|
+
e.preventDefault();
|
|
1943
|
+
if (!promptSubmitEl.disabled) startFromPrompt();
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
promptSubmitEl.addEventListener("click", () => {
|
|
1947
|
+
if (!promptSubmitEl.disabled) startFromPrompt();
|
|
1948
|
+
});
|
|
1949
|
+
syncSubmitState();
|
|
1950
|
+
const shortcutEl = document.getElementById("setup-prompt-shortcut");
|
|
1951
|
+
if (shortcutEl && !/Mac|iPhone|iPad/.test(navigator.platform)) shortcutEl.textContent = "Ctrl+↩";
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// Starter templates
|
|
1955
|
+
document.getElementById("btn-create-from-starter").addEventListener("click", createFromStarter);
|
|
1956
|
+
document.getElementById("starter-theme-name").addEventListener("keydown", (e) => {
|
|
1957
|
+
if (e.key === "Enter") { e.preventDefault(); createFromStarter(); }
|
|
1057
1958
|
});
|
|
1058
1959
|
|
|
1059
1960
|
// New theme
|
|
@@ -1451,8 +2352,7 @@ function handleRoute() {
|
|
|
1451
2352
|
if (appTemplateMatch) {
|
|
1452
2353
|
const themeName = decodeURIComponent(appTemplateMatch[1]);
|
|
1453
2354
|
const templateId = decodeURIComponent(appTemplateMatch[2]);
|
|
1454
|
-
|
|
1455
|
-
if (currentAppTheme === themeName && !appScreen.classList.contains("hidden")) return;
|
|
2355
|
+
if (currentAppTheme === themeName && appBody.dataset.mode === "editor") return;
|
|
1456
2356
|
// Open theme then activate template
|
|
1457
2357
|
openTheme(themeName).then(() => {
|
|
1458
2358
|
if (typeof showChat === "function") {
|
|
@@ -1466,24 +2366,22 @@ function handleRoute() {
|
|
|
1466
2366
|
const appMatch = hash.match(/^#\/app\/([^/]+)$/);
|
|
1467
2367
|
if (appMatch) {
|
|
1468
2368
|
const themeName = decodeURIComponent(appMatch[1]);
|
|
1469
|
-
if (currentAppTheme === themeName &&
|
|
2369
|
+
if (currentAppTheme === themeName && appBody.dataset.mode === "editor") return;
|
|
1470
2370
|
openTheme(themeName);
|
|
1471
2371
|
return;
|
|
1472
2372
|
}
|
|
1473
2373
|
|
|
1474
|
-
// #/dashboard/{themeName} → show
|
|
2374
|
+
// #/dashboard/{themeName} → show editor for theme
|
|
1475
2375
|
const dashMatch = hash.match(/^#\/dashboard\/(.+)$/);
|
|
1476
2376
|
if (dashMatch) {
|
|
1477
2377
|
const themeName = decodeURIComponent(dashMatch[1]);
|
|
1478
|
-
|
|
1479
|
-
if (currentDashboardTheme === themeName && dashEl && !dashEl.classList.contains("hidden")) return;
|
|
2378
|
+
if (currentDashboardTheme === themeName && appBody.dataset.mode === "editor") return;
|
|
1480
2379
|
openTheme(themeName);
|
|
1481
2380
|
return;
|
|
1482
2381
|
}
|
|
1483
2382
|
|
|
1484
2383
|
// Default: show setup
|
|
1485
|
-
|
|
1486
|
-
if (!appScreen.classList.contains("hidden") || (dashEl && !dashEl.classList.contains("hidden"))) {
|
|
2384
|
+
if (appBody.dataset.mode === "editor") {
|
|
1487
2385
|
showSetup();
|
|
1488
2386
|
}
|
|
1489
2387
|
}
|