vibespot 0.4.4 → 0.5.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
@@ -21,11 +21,21 @@ const ENGINE_DISPLAY_NAMES = {
21
21
 
22
22
  async function initSetup() {
23
23
  try {
24
+ // Show loading spinner in rail while fetching
25
+ const railItems = document.getElementById("project-rail-items");
26
+ if (railItems) {
27
+ railItems.innerHTML = `
28
+ <div class="project-rail__loading">
29
+ <div class="setup__spinner"></div>
30
+ <span>Loading projects...</span>
31
+ </div>`;
32
+ }
33
+
24
34
  const res = await fetch("/api/setup");
25
35
  const info = await res.json();
26
36
 
27
- // Populate sidebar with all projects (sessions + local themes)
28
- populateSidebar(info);
37
+ // Populate the project rail with all projects
38
+ populateProjectRail(info);
29
39
 
30
40
  // Auto-select engine if available but not yet chosen
31
41
  if (info.availableEngines && info.availableEngines.length > 0 && !info.activeEngine) {
@@ -118,19 +128,14 @@ async function saveAlertApiKey(key) {
118
128
  }
119
129
 
120
130
  // ---------------------------------------------------------------------------
121
- // Sidebar — project list (like Lovable)
131
+ // Project list helpers
122
132
  // ---------------------------------------------------------------------------
123
133
 
124
- function populateSidebar(info) {
125
- const list = document.getElementById("sidebar-project-list");
126
- const countEl = document.getElementById("sidebar-project-count");
127
- list.innerHTML = "";
128
-
129
- // Build a combined, deduplicated list of projects
134
+ /** Dedup sessions + local themes into a single project list */
135
+ function deduplicateProjects(info) {
130
136
  const projects = [];
131
137
  const seen = new Set();
132
138
 
133
- // Add sessions first (most recent)
134
139
  for (const s of info.sessions || []) {
135
140
  if (!seen.has(s.themeName)) {
136
141
  seen.add(s.themeName);
@@ -139,51 +144,75 @@ function populateSidebar(info) {
139
144
  type: "session",
140
145
  sessionId: s.id,
141
146
  updatedAt: s.updatedAt,
147
+ moduleCount: s.moduleCount ?? null,
148
+ templateCount: s.templateCount ?? null,
142
149
  });
143
150
  }
144
151
  }
145
152
 
146
- // Add local themes that aren't already sessions
147
- for (const name of info.localThemes || []) {
153
+ for (const t of info.localThemes || []) {
154
+ const name = typeof t === "string" ? t : t.name;
148
155
  if (!seen.has(name)) {
149
156
  seen.add(name);
150
- projects.push({ name, type: "local", sessionId: null, updatedAt: null });
157
+ projects.push({
158
+ name,
159
+ type: "local",
160
+ sessionId: null,
161
+ updatedAt: null,
162
+ moduleCount: typeof t === "object" ? t.moduleCount ?? null : null,
163
+ templateCount: null,
164
+ });
151
165
  }
152
166
  }
153
167
 
154
- countEl.textContent = projects.length;
168
+ return projects;
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Collapsible Project Rail (expanded on setup, collapsed on dashboard/chat)
173
+ // ---------------------------------------------------------------------------
174
+
175
+ const railTooltip = document.getElementById("project-rail-tooltip");
176
+
177
+ function populateProjectRail(info) {
178
+ const rail = document.getElementById("project-rail-items");
179
+ const countEl = document.getElementById("rail-project-count");
180
+ if (!rail) return;
181
+ rail.innerHTML = "";
182
+
183
+ const projects = deduplicateProjects(info);
184
+ if (countEl) countEl.textContent = projects.length;
155
185
 
156
186
  if (projects.length === 0) {
157
- list.innerHTML = `<div class="setup-sidebar__empty">No projects yet.<br>Create one to get started.</div>`;
187
+ rail.innerHTML = '<div class="project-rail__empty">No projects yet.<br>Create one to get started.</div>';
158
188
  return;
159
189
  }
160
190
 
161
191
  for (const p of projects) {
162
192
  const item = document.createElement("div");
163
- item.className = "setup-sidebar__item";
193
+ item.className = "project-rail__item";
194
+ item.dataset.name = p.name;
195
+
164
196
  const initial = p.name.charAt(0).toUpperCase();
165
197
  const meta = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
166
198
 
167
- const openBtn = document.createElement("button");
168
- openBtn.className = "setup-sidebar__item-open";
169
- openBtn.innerHTML = `
170
- <div class="setup-sidebar__item-icon">${esc(initial)}</div>
171
- <div class="setup-sidebar__item-info">
172
- <span class="setup-sidebar__item-name">${esc(p.name)}</span>
173
- <span class="setup-sidebar__item-meta">${meta}</span>
174
- </div>
175
- `;
176
- openBtn.addEventListener("click", () => {
177
- if (p.sessionId) {
178
- resumeSession(p.sessionId);
179
- } else {
180
- openTheme(p.name);
181
- }
182
- });
183
- item.appendChild(openBtn);
184
-
199
+ // Bubble (always visible — in collapsed mode this is the only thing shown)
200
+ const bubble = document.createElement("div");
201
+ bubble.className = "project-rail__item-bubble";
202
+ bubble.textContent = initial;
203
+ item.appendChild(bubble);
204
+
205
+ // Info (visible when expanded via CSS)
206
+ const infoEl = document.createElement("div");
207
+ infoEl.className = "project-rail__item-info";
208
+ infoEl.innerHTML = `
209
+ <span class="project-rail__item-name">${esc(p.name)}</span>
210
+ <span class="project-rail__item-meta">${meta}</span>`;
211
+ item.appendChild(infoEl);
212
+
213
+ // Delete button (visible when expanded + hover)
185
214
  const delBtn = document.createElement("button");
186
- delBtn.className = "setup-sidebar__item-delete";
215
+ delBtn.className = "project-rail__item-delete";
187
216
  delBtn.innerHTML = "&times;";
188
217
  delBtn.title = "Delete project";
189
218
  delBtn.addEventListener("click", (e) => {
@@ -192,10 +221,57 @@ function populateSidebar(info) {
192
221
  });
193
222
  item.appendChild(delBtn);
194
223
 
195
- list.appendChild(item);
224
+ // Tooltip (only when collapsed — skip when expanded since name is visible)
225
+ item.addEventListener("mouseenter", () => {
226
+ const railEl = document.getElementById("project-rail");
227
+ if (railEl && railEl.classList.contains("project-rail--expanded")) return;
228
+
229
+ let stats = "";
230
+ if (p.moduleCount != null) {
231
+ stats = p.moduleCount + " module" + (p.moduleCount !== 1 ? "s" : "");
232
+ if (p.templateCount > 1) stats += " \u00b7 " + p.templateCount + " templates";
233
+ stats += p.updatedAt ? " \u00b7 " + timeAgo(p.updatedAt) : " \u00b7 on disk";
234
+ } else {
235
+ stats = p.updatedAt ? timeAgo(p.updatedAt) : "on disk";
236
+ }
237
+
238
+ railTooltip.innerHTML =
239
+ '<div class="project-rail__tooltip-name">' + esc(p.name) + "</div>" +
240
+ '<div class="project-rail__tooltip-stats">' + stats + "</div>";
241
+
242
+ const rect = item.getBoundingClientRect();
243
+ railTooltip.style.top = rect.top + "px";
244
+ railTooltip.classList.add("project-rail__tooltip--visible");
245
+ });
246
+
247
+ item.addEventListener("mouseleave", () => {
248
+ railTooltip.classList.remove("project-rail__tooltip--visible");
249
+ });
250
+
251
+ // Click to open
252
+ item.addEventListener("click", () => {
253
+ if (p.sessionId) resumeSession(p.sessionId);
254
+ else openTheme(p.name);
255
+ });
256
+
257
+ rail.appendChild(item);
196
258
  }
259
+
260
+ updateRailActive();
197
261
  }
198
262
 
263
+ function updateRailActive() {
264
+ const current = currentAppTheme || currentDashboardTheme || "";
265
+ document.querySelectorAll(".project-rail__item").forEach((btn) => {
266
+ btn.classList.toggle("project-rail__item--active", btn.dataset.name === current);
267
+ });
268
+ }
269
+
270
+ // "+" button → go to setup
271
+ document.getElementById("project-rail-add")?.addEventListener("click", () => {
272
+ showSetup();
273
+ });
274
+
199
275
  // ---------------------------------------------------------------------------
200
276
  // Delete project confirmation
201
277
  // ---------------------------------------------------------------------------
@@ -568,6 +644,7 @@ function showApp(themeName) {
568
644
  if (typeof showDashboard === "function") {
569
645
  currentAppTheme = themeName;
570
646
  showDashboard(themeName);
647
+ updateRailActive();
571
648
  } else {
572
649
  // Fallback if dashboard.js not loaded
573
650
  showAppDirect(themeName);
@@ -581,6 +658,7 @@ function showApp(themeName) {
581
658
  function showAppDirect(themeName) {
582
659
  setupScreen.classList.add("hidden");
583
660
  document.getElementById("setup-topbar").classList.add("hidden");
661
+ document.getElementById("project-rail")?.classList.remove("project-rail--expanded");
584
662
  if (typeof hideDashboard === "function") hideDashboard();
585
663
  appScreen.classList.remove("hidden");
586
664
  document.getElementById("theme-name").textContent = themeName;
@@ -600,6 +678,7 @@ function showAppDirect(themeName) {
600
678
  if (typeof refreshPreview === "function") {
601
679
  refreshPreview();
602
680
  }
681
+ updateRailActive();
603
682
  }
604
683
 
605
684
  function showSetup() {
@@ -607,9 +686,11 @@ function showSetup() {
607
686
  if (typeof hideDashboard === "function") hideDashboard();
608
687
  setupScreen.classList.remove("hidden");
609
688
  document.getElementById("setup-topbar").classList.remove("hidden");
689
+ document.getElementById("project-rail")?.classList.add("project-rail--expanded");
610
690
  currentAppTheme = "";
611
691
 
612
692
  hideLoading();
693
+ updateRailActive();
613
694
 
614
695
  if (location.hash && location.hash !== "#/") {
615
696
  history.pushState(null, "", "#/");