vibespot 1.1.0 → 1.2.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 +11 -1
- package/assets/plan-templates/agency-services.md +42 -0
- package/assets/plan-templates/blog-content-hub.md +41 -0
- package/assets/plan-templates/ecommerce-product.md +42 -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 +259 -228
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
- 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/ui/chat.js +865 -130
- package/ui/dashboard.js +194 -12
- package/ui/docs/index.html +89 -10
- package/ui/field-editor.js +1 -1
- package/ui/index.html +156 -37
- package/ui/marketplace.js +218 -0
- package/ui/plan.js +0 -0
- package/ui/preview.js +316 -1
- package/ui/settings.js +35 -21
- package/ui/setup.js +291 -3
- package/ui/styles.css +1305 -120
package/ui/setup.js
CHANGED
|
@@ -52,6 +52,9 @@ async function initSetup() {
|
|
|
52
52
|
// Populate the project rail with all projects
|
|
53
53
|
populateProjectRail(info);
|
|
54
54
|
|
|
55
|
+
// Show "Continue where you left off" cards above the create options
|
|
56
|
+
populateRecentProjects(info);
|
|
57
|
+
|
|
55
58
|
// Auto-select engine if available but not yet chosen
|
|
56
59
|
if (info.availableEngines && info.availableEngines.length > 0 && !info.activeEngine) {
|
|
57
60
|
const engine = info.availableEngines[0];
|
|
@@ -104,6 +107,13 @@ async function initSetup() {
|
|
|
104
107
|
// Reset panel state
|
|
105
108
|
remoteThemesLoaded = false;
|
|
106
109
|
|
|
110
|
+
// Reset starter cache so each visit re-fetches from server
|
|
111
|
+
_startersCache = null;
|
|
112
|
+
|
|
113
|
+
// Auto-expand the starter template panel so templates are visible by default
|
|
114
|
+
activePanel = null;
|
|
115
|
+
togglePanel("starter");
|
|
116
|
+
|
|
107
117
|
} catch (err) {
|
|
108
118
|
showError("Could not connect to server. Is vibeSpot running?");
|
|
109
119
|
}
|
|
@@ -181,6 +191,62 @@ function deduplicateProjects(info) {
|
|
|
181
191
|
return projects;
|
|
182
192
|
}
|
|
183
193
|
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// "Continue where you left off" — recent projects above the create options
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
const RECENT_PROJECTS_LIMIT = 4;
|
|
199
|
+
|
|
200
|
+
function populateRecentProjects(info) {
|
|
201
|
+
const section = document.getElementById("setup-recent");
|
|
202
|
+
const list = document.getElementById("setup-recent-list");
|
|
203
|
+
const viewAll = document.getElementById("setup-recent-all");
|
|
204
|
+
if (!section || !list) return;
|
|
205
|
+
|
|
206
|
+
const projects = deduplicateProjects(info);
|
|
207
|
+
if (projects.length === 0) {
|
|
208
|
+
section.classList.add("hidden");
|
|
209
|
+
list.innerHTML = "";
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Most recently updated first; locals (no updatedAt) follow
|
|
214
|
+
const withTime = projects.filter((p) => p.updatedAt).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
215
|
+
const withoutTime = projects.filter((p) => !p.updatedAt);
|
|
216
|
+
const ordered = [...withTime, ...withoutTime];
|
|
217
|
+
const top = ordered.slice(0, RECENT_PROJECTS_LIMIT);
|
|
218
|
+
|
|
219
|
+
list.innerHTML = "";
|
|
220
|
+
for (const p of top) {
|
|
221
|
+
const card = document.createElement("button");
|
|
222
|
+
card.type = "button";
|
|
223
|
+
card.className = "setup__recent-card";
|
|
224
|
+
|
|
225
|
+
const initial = p.name.charAt(0).toUpperCase();
|
|
226
|
+
const meta = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
|
|
227
|
+
|
|
228
|
+
card.innerHTML =
|
|
229
|
+
`<span class="setup__recent-card-bubble">${esc(initial)}</span>` +
|
|
230
|
+
`<span class="setup__recent-card-text">` +
|
|
231
|
+
`<span class="setup__recent-card-name">${esc(p.name)}</span>` +
|
|
232
|
+
`<span class="setup__recent-card-meta">${esc(meta)}</span>` +
|
|
233
|
+
`</span>`;
|
|
234
|
+
|
|
235
|
+
card.addEventListener("click", () => {
|
|
236
|
+
if (typeof isStreaming !== "undefined" && isStreaming) {
|
|
237
|
+
showError("Cannot switch projects while AI is generating.");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (p.sessionId) resumeSession(p.sessionId);
|
|
241
|
+
else openTheme(p.name);
|
|
242
|
+
});
|
|
243
|
+
list.appendChild(card);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (viewAll) viewAll.classList.toggle("hidden", projects.length <= top.length);
|
|
247
|
+
section.classList.remove("hidden");
|
|
248
|
+
}
|
|
249
|
+
|
|
184
250
|
// ---------------------------------------------------------------------------
|
|
185
251
|
// Collapsible Project Rail (expanded on setup, collapsed on dashboard/chat)
|
|
186
252
|
// ---------------------------------------------------------------------------
|
|
@@ -250,7 +316,7 @@ function populateProjectRail(info) {
|
|
|
250
316
|
|
|
251
317
|
let stats = "";
|
|
252
318
|
if (p.moduleCount != null) {
|
|
253
|
-
stats = p.moduleCount + "
|
|
319
|
+
stats = p.moduleCount + " section" + (p.moduleCount !== 1 ? "s" : "");
|
|
254
320
|
if (p.templateCount > 1) stats += " \u00b7 " + p.templateCount + " templates";
|
|
255
321
|
stats += p.updatedAt ? " \u00b7 " + timeAgo(p.updatedAt) : " \u00b7 on disk";
|
|
256
322
|
} else {
|
|
@@ -664,6 +730,167 @@ async function createTheme() {
|
|
|
664
730
|
}
|
|
665
731
|
}
|
|
666
732
|
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
// Primary path: "Describe the landing page you want to build..."
|
|
735
|
+
// Creates a fresh theme and forwards the prompt to the chat once it connects.
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
|
|
738
|
+
function generateThemeNameFromPrompt(prompt) {
|
|
739
|
+
const slug = prompt
|
|
740
|
+
.toLowerCase()
|
|
741
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
742
|
+
.trim()
|
|
743
|
+
.split(/\s+/)
|
|
744
|
+
.slice(0, 5)
|
|
745
|
+
.join("-")
|
|
746
|
+
.replace(/-+/g, "-")
|
|
747
|
+
.replace(/^-|-$/g, "")
|
|
748
|
+
.slice(0, 40);
|
|
749
|
+
|
|
750
|
+
if (slug) return slug;
|
|
751
|
+
return "page-" + Date.now().toString(36);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function startFromPrompt() {
|
|
755
|
+
const input = document.getElementById("setup-prompt-input");
|
|
756
|
+
const submitBtn = document.getElementById("setup-prompt-submit");
|
|
757
|
+
const prompt = (input?.value || "").trim();
|
|
758
|
+
if (!prompt) {
|
|
759
|
+
input?.focus();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
764
|
+
const themeName = generateThemeNameFromPrompt(prompt);
|
|
765
|
+
showLoading("Creating theme...");
|
|
766
|
+
|
|
767
|
+
try {
|
|
768
|
+
const res = await fetch("/api/setup/create", {
|
|
769
|
+
method: "POST",
|
|
770
|
+
headers: { "Content-Type": "application/json" },
|
|
771
|
+
body: JSON.stringify({ name: themeName }),
|
|
772
|
+
});
|
|
773
|
+
const data = await res.json();
|
|
774
|
+
|
|
775
|
+
if (data.error) {
|
|
776
|
+
showError(data.error);
|
|
777
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// chat.js will pick this up on the next ws "init" message
|
|
782
|
+
window.__pendingInitialPrompt = prompt;
|
|
783
|
+
if (input) input.value = "";
|
|
784
|
+
showAppDirect(data.themeName);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
showError("Failed to create theme: " + err.message);
|
|
787
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ---------------------------------------------------------------------------
|
|
792
|
+
// Starter templates
|
|
793
|
+
// ---------------------------------------------------------------------------
|
|
794
|
+
|
|
795
|
+
let _startersCache = null;
|
|
796
|
+
let _selectedStarterId = null;
|
|
797
|
+
|
|
798
|
+
function escHtml(s) {
|
|
799
|
+
const d = document.createElement("div");
|
|
800
|
+
d.textContent = s;
|
|
801
|
+
return d.innerHTML;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function loadStarterGrid() {
|
|
805
|
+
const grid = document.getElementById("starter-grid");
|
|
806
|
+
if (!grid) return;
|
|
807
|
+
|
|
808
|
+
if (_startersCache !== null) {
|
|
809
|
+
renderStarterGrid(_startersCache);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
try {
|
|
814
|
+
const res = await fetch("/api/starters");
|
|
815
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
816
|
+
const data = await res.json();
|
|
817
|
+
_startersCache = data.starters || [];
|
|
818
|
+
renderStarterGrid(_startersCache);
|
|
819
|
+
} catch {
|
|
820
|
+
// API unavailable — attach click listeners to any hardcoded static cards
|
|
821
|
+
grid.querySelectorAll(".starter-card").forEach((card) => {
|
|
822
|
+
card.addEventListener("click", () => selectStarter(card.dataset.starterId));
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function renderStarterGrid(starters) {
|
|
828
|
+
const grid = document.getElementById("starter-grid");
|
|
829
|
+
if (!grid) return;
|
|
830
|
+
|
|
831
|
+
if (starters.length === 0) {
|
|
832
|
+
grid.innerHTML = '<p class="setup__hint">No starter templates available.</p>';
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
grid.innerHTML = starters.map((s) => `
|
|
837
|
+
<div class="starter-card${_selectedStarterId === s.id ? " selected" : ""}" data-starter-id="${escHtml(s.id)}">
|
|
838
|
+
<span class="starter-card__name">${escHtml(s.name)}</span>
|
|
839
|
+
<span class="starter-card__desc">${escHtml(s.description)}</span>
|
|
840
|
+
<span class="starter-card__meta">${s.moduleCount} modules</span>
|
|
841
|
+
</div>
|
|
842
|
+
`).join("");
|
|
843
|
+
|
|
844
|
+
grid.querySelectorAll(".starter-card").forEach((card) => {
|
|
845
|
+
card.addEventListener("click", () => selectStarter(card.dataset.starterId));
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function selectStarter(id) {
|
|
850
|
+
_selectedStarterId = id;
|
|
851
|
+
const starter = (_startersCache || []).find((s) => s.id === id);
|
|
852
|
+
if (!starter) return;
|
|
853
|
+
|
|
854
|
+
document.querySelectorAll(".starter-card").forEach((c) => c.classList.toggle("selected", c.dataset.starterId === id));
|
|
855
|
+
|
|
856
|
+
const createSection = document.getElementById("starter-create");
|
|
857
|
+
const label = document.getElementById("starter-create-label");
|
|
858
|
+
if (createSection && label) {
|
|
859
|
+
label.textContent = `Create theme from "${starter.name}":`;
|
|
860
|
+
createSection.classList.remove("hidden");
|
|
861
|
+
setTimeout(() => document.getElementById("starter-theme-name")?.focus(), 50);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function createFromStarter() {
|
|
866
|
+
if (!_selectedStarterId) return;
|
|
867
|
+
const name = document.getElementById("starter-theme-name").value.trim();
|
|
868
|
+
if (!name) {
|
|
869
|
+
showError("Please enter a name for your theme.");
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
showLoading("Creating theme from template...");
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
const res = await fetch("/api/setup/create", {
|
|
877
|
+
method: "POST",
|
|
878
|
+
headers: { "Content-Type": "application/json" },
|
|
879
|
+
body: JSON.stringify({ name, starterId: _selectedStarterId }),
|
|
880
|
+
});
|
|
881
|
+
const data = await res.json();
|
|
882
|
+
|
|
883
|
+
if (data.error) {
|
|
884
|
+
showError(data.error);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
showApp(data.themeName);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
showError("Failed to create theme: " + err.message);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
667
894
|
async function fetchTheme() {
|
|
668
895
|
const name = document.getElementById("fetch-theme-name").value.trim();
|
|
669
896
|
if (!name) {
|
|
@@ -884,7 +1111,7 @@ function togglePanel(action) {
|
|
|
884
1111
|
panels.forEach((p) => p.classList.add("hidden"));
|
|
885
1112
|
buttons.forEach((b) => b.classList.remove("active"));
|
|
886
1113
|
|
|
887
|
-
const panelMap = { new: "panel-new", continue: "panel-continue", download: "panel-download", figma: "panel-figma", convert: "panel-convert" };
|
|
1114
|
+
const panelMap = { starter: "panel-starter", new: "panel-new", continue: "panel-continue", download: "panel-download", figma: "panel-figma", convert: "panel-convert" };
|
|
888
1115
|
const panel = document.getElementById(panelMap[action]);
|
|
889
1116
|
if (panel) {
|
|
890
1117
|
panel.classList.remove("hidden");
|
|
@@ -896,6 +1123,7 @@ function togglePanel(action) {
|
|
|
896
1123
|
if (btn) btn.classList.add("active");
|
|
897
1124
|
|
|
898
1125
|
// Focus input if applicable
|
|
1126
|
+
if (action === "starter") loadStarterGrid();
|
|
899
1127
|
if (action === "new") setTimeout(() => document.getElementById("new-theme-name")?.focus(), 50);
|
|
900
1128
|
if (action === "convert") setTimeout(() => document.getElementById("import-url")?.focus(), 50);
|
|
901
1129
|
if (action === "figma") initFigmaPanel();
|
|
@@ -1051,11 +1279,71 @@ async function downloadThemeByName() {
|
|
|
1051
1279
|
// Event listeners
|
|
1052
1280
|
// ---------------------------------------------------------------------------
|
|
1053
1281
|
|
|
1054
|
-
// Action buttons
|
|
1282
|
+
// Action buttons (advanced "More ways to start" panel)
|
|
1055
1283
|
document.querySelectorAll(".setup__action-btn").forEach((btn) => {
|
|
1056
1284
|
btn.addEventListener("click", () => togglePanel(btn.dataset.action));
|
|
1057
1285
|
});
|
|
1058
1286
|
|
|
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
|
+
});
|
|
1296
|
+
});
|
|
1297
|
+
|
|
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);
|
|
1307
|
+
}
|
|
1308
|
+
document.getElementById("setup-more-toggle")?.addEventListener("click", () => expandMoreOptions());
|
|
1309
|
+
|
|
1310
|
+
// "View all" link in recent projects → open the full Continue panel
|
|
1311
|
+
document.getElementById("setup-recent-all")?.addEventListener("click", () => {
|
|
1312
|
+
togglePanel("continue");
|
|
1313
|
+
setTimeout(() => {
|
|
1314
|
+
document.getElementById("panel-continue")?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1315
|
+
}, 60);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// Primary "describe-it" prompt
|
|
1319
|
+
const promptInputEl = document.getElementById("setup-prompt-input");
|
|
1320
|
+
const promptSubmitEl = document.getElementById("setup-prompt-submit");
|
|
1321
|
+
if (promptInputEl && promptSubmitEl) {
|
|
1322
|
+
const syncSubmitState = () => {
|
|
1323
|
+
promptSubmitEl.disabled = promptInputEl.value.trim().length === 0;
|
|
1324
|
+
};
|
|
1325
|
+
promptInputEl.addEventListener("input", syncSubmitState);
|
|
1326
|
+
promptInputEl.addEventListener("keydown", (e) => {
|
|
1327
|
+
// ⌘/Ctrl + Enter submits; plain Enter inserts newline like a normal textarea.
|
|
1328
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
1329
|
+
e.preventDefault();
|
|
1330
|
+
if (!promptSubmitEl.disabled) startFromPrompt();
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
promptSubmitEl.addEventListener("click", () => {
|
|
1334
|
+
if (!promptSubmitEl.disabled) startFromPrompt();
|
|
1335
|
+
});
|
|
1336
|
+
syncSubmitState();
|
|
1337
|
+
const shortcutEl = document.getElementById("setup-prompt-shortcut");
|
|
1338
|
+
if (shortcutEl && !/Mac|iPhone|iPad/.test(navigator.platform)) shortcutEl.textContent = "Ctrl+↩";
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Starter templates
|
|
1342
|
+
document.getElementById("btn-create-from-starter").addEventListener("click", createFromStarter);
|
|
1343
|
+
document.getElementById("starter-theme-name").addEventListener("keydown", (e) => {
|
|
1344
|
+
if (e.key === "Enter") { e.preventDefault(); createFromStarter(); }
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1059
1347
|
// New theme
|
|
1060
1348
|
document.getElementById("btn-create-theme").addEventListener("click", createTheme);
|
|
1061
1349
|
document.getElementById("new-theme-name").addEventListener("keydown", (e) => {
|