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/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 + " module" + (p.moduleCount !== 1 ? "s" : "");
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) => {