vibespot 0.6.0 → 0.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibespot",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "AI-powered HubSpot CMS landing page builder — vibe coding & React converter",
5
5
  "type": "module",
6
6
  "bin": {
package/ui/chat.js CHANGED
@@ -12,6 +12,11 @@ let streamingMsgEl = null;
12
12
  let streamBuffer = "";
13
13
  let streamStartTime = 0;
14
14
  let streamTimerInterval = null;
15
+ let lastStreamStatus = "";
16
+ let currentSessionId = "";
17
+ let currentTemplateId = "";
18
+ let renderScheduled = false;
19
+ let scrollScheduled = false;
15
20
 
16
21
  const messagesEl = document.getElementById("chat-messages");
17
22
  const inputEl = document.getElementById("chat-input");
@@ -44,11 +49,15 @@ function connectWebSocket() {
44
49
  };
45
50
 
46
51
  ws.onclose = () => {
52
+ stopStreamTimer();
53
+ if (isStreaming) finishStreaming();
47
54
  setStatus("Disconnected — reconnecting...");
48
55
  setTimeout(connectWebSocket, 2000);
49
56
  };
50
57
 
51
58
  ws.onerror = () => {
59
+ stopStreamTimer();
60
+ if (isStreaming) finishStreaming();
52
61
  setStatus("Connection error");
53
62
  };
54
63
  }
@@ -64,6 +73,8 @@ function handleWsMessage(msg) {
64
73
 
65
74
  switch (msg.type) {
66
75
  case "init":
76
+ currentSessionId = msg.sessionId || "";
77
+ currentTemplateId = msg.templateId || "";
67
78
  document.getElementById("theme-name").textContent = msg.themeName || "—";
68
79
 
69
80
  // Clear previous project's chat and module list
@@ -104,7 +115,6 @@ function handleWsMessage(msg) {
104
115
  break;
105
116
 
106
117
  case "stream":
107
- clearStreamStatus();
108
118
  handleStreamChunk(msg.content);
109
119
  break;
110
120
 
@@ -199,6 +209,7 @@ function appendUserMessage(text, timestamp) {
199
209
  function startStreaming() {
200
210
  isStreaming = true;
201
211
  streamBuffer = "";
212
+ lastStreamStatus = "";
202
213
  sendBtn.disabled = true;
203
214
  streamStartTime = Date.now();
204
215
 
@@ -231,14 +242,42 @@ function handleStreamChunk(text) {
231
242
  if (!streamingMsgEl) return;
232
243
  streamBuffer += text;
233
244
 
234
- // Render markdown-lite (code blocks, inline code, paragraphs)
235
- streamingMsgEl.innerHTML = renderMarkdown(streamBuffer);
245
+ if (!renderScheduled) {
246
+ renderScheduled = true;
247
+ requestAnimationFrame(flushStreamRender);
248
+ }
249
+ }
250
+
251
+ function flushStreamRender() {
252
+ renderScheduled = false;
253
+ if (!streamingMsgEl) return;
254
+
255
+ // Hide incomplete code fences (AI is writing module code)
256
+ let display = streamBuffer;
257
+ const fenceCount = (display.match(/```/g) || []).length;
258
+ if (fenceCount % 2 !== 0) {
259
+ const lastFence = display.lastIndexOf("```");
260
+ display = display.substring(0, lastFence);
261
+ }
262
+
263
+ const rendered = renderMarkdown(display);
264
+ const visibleText = rendered.replace(/<[^>]*>/g, "").trim();
265
+
266
+ if (visibleText) {
267
+ // Preserve the stream-status spinner while updating text
268
+ const statusEl = streamingMsgEl.querySelector(".stream-status");
269
+ streamingMsgEl.innerHTML = rendered;
270
+ if (statusEl) streamingMsgEl.appendChild(statusEl);
271
+ }
272
+ // No visible text — leave the spinner (.stream-status) untouched in the DOM
236
273
  scrollToBottom();
237
274
  }
238
275
 
239
276
  function handleStreamStatus(status) {
240
277
  if (!streamingMsgEl) startStreaming();
241
278
 
279
+ lastStreamStatus = status;
280
+
242
281
  // Find or create the status element inside the streaming bubble
243
282
  let statusEl = streamingMsgEl.querySelector(".stream-status");
244
283
  if (!statusEl) {
@@ -311,7 +350,9 @@ function finishStreaming() {
311
350
 
312
351
  // Final render of the full response
313
352
  if (streamingMsgEl && streamBuffer) {
314
- streamingMsgEl.innerHTML = renderMarkdown(streamBuffer);
353
+ const rendered = renderMarkdown(streamBuffer);
354
+ const visibleText = rendered.replace(/<[^>]*>/g, "").trim();
355
+ streamingMsgEl.innerHTML = visibleText ? rendered : "<em>Modules applied.</em>";
315
356
  }
316
357
 
317
358
  streamingMsgEl = null;
@@ -344,13 +385,10 @@ function appendAssistantError(message) {
344
385
  // ---------------------------------------------------------------------------
345
386
 
346
387
  function renderMarkdown(text) {
347
- // Hide vibespot-modules JSON blocks (they're data, not display)
348
- text = text.replace(/```vibespot-modules[\s\S]*?```/g, "");
349
-
350
- // Code blocks: ```lang\n...\n```
351
- text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
352
- return `<pre><code>${escapeHtml(code.trim())}</code></pre>`;
353
- });
388
+ // Strip all code blocks module code is applied via JSON, not displayed in chat
389
+ text = text.replace(/```[\s\S]*?```/g, "");
390
+ // Also strip unclosed code fences (truncated responses)
391
+ text = text.replace(/```[\s\S]*$/g, "");
354
392
 
355
393
  // Inline code: `...`
356
394
  text = text.replace(/`([^`]+)`/g, "<code>$1</code>");
@@ -388,7 +426,16 @@ function escapeHtml(str) {
388
426
  }
389
427
 
390
428
  function scrollToBottom() {
391
- messagesEl.scrollTop = messagesEl.scrollHeight;
429
+ if (scrollScheduled) return;
430
+ scrollScheduled = true;
431
+ requestAnimationFrame(() => {
432
+ scrollScheduled = false;
433
+ // Only auto-scroll if user is near the bottom (within 150px)
434
+ const gap = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
435
+ if (gap < 150) {
436
+ messagesEl.scrollTop = messagesEl.scrollHeight;
437
+ }
438
+ });
392
439
  }
393
440
 
394
441
  function setStatus(text) {
@@ -434,28 +481,50 @@ function toggleHistoryPanel() {
434
481
  if (historyPanelOpen) refreshHistoryPanel();
435
482
  }
436
483
 
484
+ let historyShowAll = false;
485
+
437
486
  async function refreshHistoryPanel() {
438
487
  const list = document.getElementById("history-list");
439
488
  if (!list) return;
440
489
  list.innerHTML = '<div class="history__loading">Loading...</div>';
441
490
 
442
491
  try {
443
- const res = await fetch("/api/history");
492
+ const useFilter = currentTemplateId && !historyShowAll;
493
+ const url = useFilter
494
+ ? `/api/history?templateId=${encodeURIComponent(currentTemplateId)}`
495
+ : "/api/history";
496
+ const res = await fetch(url);
444
497
  const data = await res.json();
445
498
 
446
499
  if (!data.available) {
447
500
  list.innerHTML = '<div class="history__empty">Git not available</div>';
448
501
  return;
449
502
  }
503
+
504
+ // Show all / filter toggle
505
+ const toggleHtml = currentTemplateId
506
+ ? `<div class="history__toggle"><button class="history__toggle-btn" id="history-toggle-filter">${historyShowAll ? "This template" : "Show all"}</button></div>`
507
+ : "";
508
+
450
509
  if (data.commits.length === 0) {
451
- list.innerHTML = '<div class="history__empty">No versions yet</div>';
510
+ list.innerHTML = toggleHtml + '<div class="history__empty">No versions yet</div>';
511
+ attachHistoryToggle();
452
512
  return;
453
513
  }
454
514
 
455
- list.innerHTML = "";
456
- for (const commit of data.commits) {
515
+ list.innerHTML = toggleHtml;
516
+ const HISTORY_LIMIT = 50;
517
+ const commits = data.commits.slice(0, HISTORY_LIMIT);
518
+ const frag = document.createDocumentFragment();
519
+
520
+ for (const commit of commits) {
457
521
  const isInitial = commit.message.startsWith("Initial ");
458
- const isRollback = commit.message.startsWith("Rollback to:");
522
+ const isRollback = commit.message.includes("Rollback to:");
523
+
524
+ // Strip [templateId] prefix from display
525
+ let displayMsg = commit.message;
526
+ const prefixMatch = displayMsg.match(/^\[[^\]]+\]\s*/);
527
+ if (prefixMatch) displayMsg = displayMsg.slice(prefixMatch[0].length);
459
528
 
460
529
  const item = document.createElement("div");
461
530
  item.className = "history-item" + (isRollback ? " history-item--rollback" : "");
@@ -464,30 +533,55 @@ async function refreshHistoryPanel() {
464
533
  <span class="history-item__hash">${escapeHtml(commit.hash)}</span>
465
534
  <span class="history-item__date">${timeAgoShort(commit.timestamp)}</span>
466
535
  </div>
467
- <div class="history-item__msg">${escapeHtml(commit.message)}</div>
536
+ <div class="history-item__msg">${escapeHtml(displayMsg)}</div>
468
537
  ${!isInitial ? `<button class="history-item__rollback" data-hash="${escapeHtml(commit.fullHash)}">Restore</button>` : ""}
469
538
  `;
470
- list.appendChild(item);
539
+ frag.appendChild(item);
540
+ }
541
+ list.appendChild(frag);
542
+
543
+ if (data.commits.length > HISTORY_LIMIT) {
544
+ const more = document.createElement("div");
545
+ more.className = "history__show-more";
546
+ more.textContent = `Showing ${HISTORY_LIMIT} of ${data.commits.length} versions`;
547
+ list.appendChild(more);
471
548
  }
472
549
 
473
550
  list.querySelectorAll(".history-item__rollback").forEach((btn) => {
474
551
  btn.addEventListener("click", () => doRollback(btn.dataset.hash));
475
552
  });
553
+ attachHistoryToggle();
476
554
  } catch {
477
555
  list.innerHTML = '<div class="history__empty">Error loading history</div>';
478
556
  }
479
557
  }
480
558
 
559
+ function attachHistoryToggle() {
560
+ const btn = document.getElementById("history-toggle-filter");
561
+ if (btn) {
562
+ btn.addEventListener("click", () => {
563
+ historyShowAll = !historyShowAll;
564
+ refreshHistoryPanel();
565
+ });
566
+ }
567
+ }
568
+
481
569
  async function doRollback(hash) {
482
- const ok = await vibeConfirm("Restore this version?", "Your current files will be replaced, but chat history is preserved.", { confirmLabel: "Restore", confirmClass: "btn--primary" });
570
+ const scoped = currentTemplateId && !historyShowAll;
571
+ const msg = scoped
572
+ ? "This template's modules will be restored to the selected version. Other templates are not affected."
573
+ : "All theme files will be replaced, but chat history is preserved.";
574
+ const ok = await vibeConfirm("Restore this version?", msg, { confirmLabel: "Restore", confirmClass: "btn--primary" });
483
575
  if (!ok) return;
484
576
  setStatus("Rolling back...");
485
577
 
486
578
  try {
579
+ const payload = { hash };
580
+ if (scoped) payload.templateId = currentTemplateId;
487
581
  const res = await fetch("/api/rollback", {
488
582
  method: "POST",
489
583
  headers: { "Content-Type": "application/json" },
490
- body: JSON.stringify({ hash }),
584
+ body: JSON.stringify(payload),
491
585
  });
492
586
  const data = await res.json();
493
587
 
@@ -991,6 +1085,83 @@ async function fetchHsAccountStatus() {
991
1085
  }
992
1086
  }
993
1087
 
1088
+ // ---------------------------------------------------------------------------
1089
+ // Topbar theme-name rename (double-click)
1090
+ // ---------------------------------------------------------------------------
1091
+
1092
+ document.getElementById("theme-name")?.addEventListener("dblclick", () => {
1093
+ const el = document.getElementById("theme-name");
1094
+ if (!el || !currentSessionId) return;
1095
+ if (el.contentEditable === "true") return;
1096
+
1097
+ const oldName = el.textContent.trim();
1098
+ el.contentEditable = "true";
1099
+ el.classList.add("topbar__project-pill--editing");
1100
+ el.focus();
1101
+
1102
+ const range = document.createRange();
1103
+ range.selectNodeContents(el);
1104
+ const sel = window.getSelection();
1105
+ sel.removeAllRanges();
1106
+ sel.addRange(range);
1107
+
1108
+ function commit() {
1109
+ el.contentEditable = "false";
1110
+ el.classList.remove("topbar__project-pill--editing");
1111
+
1112
+ const newName = el.textContent.trim();
1113
+ if (!newName || newName === oldName) {
1114
+ el.textContent = oldName;
1115
+ return;
1116
+ }
1117
+
1118
+ fetch("/api/themes/rename", {
1119
+ method: "POST",
1120
+ headers: { "Content-Type": "application/json" },
1121
+ body: JSON.stringify({ sessionId: currentSessionId, newName }),
1122
+ })
1123
+ .then((r) => r.json())
1124
+ .then((data) => {
1125
+ if (data.ok) {
1126
+ el.textContent = data.newName;
1127
+ if (typeof currentAppTheme !== "undefined") currentAppTheme = data.newName;
1128
+ window.location.hash = "#/app/" + encodeURIComponent(data.newName);
1129
+ // Update rail item
1130
+ const railItem = document.querySelector(`.project-rail__item[data-name="${oldName}"]`);
1131
+ if (railItem) {
1132
+ railItem.dataset.name = data.newName;
1133
+ const nameSpan = railItem.querySelector(".project-rail__item-name");
1134
+ if (nameSpan) nameSpan.textContent = data.newName;
1135
+ const bubble = railItem.querySelector(".project-rail__item-bubble");
1136
+ if (bubble) bubble.textContent = data.newName.charAt(0).toUpperCase();
1137
+ }
1138
+ if (typeof updateRailActive === "function") updateRailActive();
1139
+ } else {
1140
+ el.textContent = oldName;
1141
+ showError(data.error || "Rename failed");
1142
+ }
1143
+ })
1144
+ .catch(() => {
1145
+ el.textContent = oldName;
1146
+ showError("Rename failed");
1147
+ });
1148
+ }
1149
+
1150
+ el.addEventListener("blur", commit, { once: true });
1151
+ el.addEventListener("keydown", function handler(e) {
1152
+ if (e.key === "Enter") {
1153
+ e.preventDefault();
1154
+ el.removeEventListener("keydown", handler);
1155
+ el.blur();
1156
+ }
1157
+ if (e.key === "Escape") {
1158
+ el.textContent = oldName;
1159
+ el.removeEventListener("keydown", handler);
1160
+ el.blur();
1161
+ }
1162
+ });
1163
+ });
1164
+
994
1165
  // ---------------------------------------------------------------------------
995
1166
  // Initialize
996
1167
  // ---------------------------------------------------------------------------
package/ui/dashboard.js CHANGED
@@ -25,6 +25,7 @@ const PAGE_TYPE_FULL_LABELS = {
25
25
  // ---------------------------------------------------------------------------
26
26
 
27
27
  let currentDashboardTheme = "";
28
+ let currentDashboardSessionId = "";
28
29
 
29
30
  async function showDashboard(themeName) {
30
31
  currentDashboardTheme = themeName;
@@ -37,6 +38,15 @@ async function showDashboard(themeName) {
37
38
  dashboardScreen.classList.remove("hidden");
38
39
 
39
40
  document.getElementById("dashboard-theme-name").textContent = themeName;
41
+ document.getElementById("dashboard-theme-heading").textContent = themeName;
42
+ document.getElementById("dashboard-theme-path-text").textContent = "";
43
+
44
+ // Get sessionId for the active theme
45
+ try {
46
+ const themesRes = await fetch("/api/themes");
47
+ const themesData = await themesRes.json();
48
+ currentDashboardSessionId = themesData.activeTheme?.id || "";
49
+ } catch { currentDashboardSessionId = ""; }
40
50
 
41
51
  // Update URL
42
52
  const target = "#/dashboard/" + encodeURIComponent(themeName);
@@ -69,6 +79,9 @@ async function refreshDashboard() {
69
79
  renderTemplateList(data.templates || []);
70
80
  renderModuleLibrary(data.moduleLibrary || []);
71
81
  renderBrandAssets(data.brandAssets || {});
82
+ if (data.themePath) {
83
+ document.getElementById("dashboard-theme-path-text").textContent = data.themePath;
84
+ }
72
85
  } catch (err) {
73
86
  console.error("Failed to load dashboard:", err);
74
87
  }
@@ -102,12 +115,78 @@ function renderTemplateList(templates) {
102
115
  list.appendChild(item);
103
116
  }
104
117
 
105
- // Attach click handlers
106
- list.querySelectorAll(".dashboard__template-open").forEach((btn) => {
107
- btn.addEventListener("click", () => openTemplate(btn.dataset.id));
108
- });
109
- list.querySelectorAll(".dashboard__template-delete").forEach((btn) => {
110
- btn.addEventListener("click", () => confirmDeleteTemplate(btn.dataset.id));
118
+ // Event delegation — single listener handles all template actions
119
+ list.onclick = (e) => {
120
+ const openBtn = e.target.closest(".dashboard__template-open");
121
+ if (openBtn) return openTemplate(openBtn.dataset.id);
122
+ const delBtn = e.target.closest(".dashboard__template-delete");
123
+ if (delBtn) return confirmDeleteTemplate(delBtn.dataset.id);
124
+ };
125
+ list.ondblclick = (e) => {
126
+ const labelEl = e.target.closest(".dashboard__template-label");
127
+ if (!labelEl) return;
128
+ const item = labelEl.closest(".dashboard__template-item");
129
+ const templateId = item?.querySelector(".dashboard__template-open")?.dataset.id;
130
+ if (templateId) startTemplateRename(labelEl, templateId);
131
+ };
132
+ }
133
+
134
+ function startTemplateRename(labelEl, templateId) {
135
+ if (labelEl.contentEditable === "true") return;
136
+
137
+ const oldLabel = labelEl.textContent.trim();
138
+ labelEl.contentEditable = "true";
139
+ labelEl.classList.add("dashboard__template-label--editing");
140
+ labelEl.focus();
141
+
142
+ const range = document.createRange();
143
+ range.selectNodeContents(labelEl);
144
+ const sel = window.getSelection();
145
+ sel.removeAllRanges();
146
+ sel.addRange(range);
147
+
148
+ function commit() {
149
+ labelEl.contentEditable = "false";
150
+ labelEl.classList.remove("dashboard__template-label--editing");
151
+
152
+ const newLabel = labelEl.textContent.trim();
153
+ if (!newLabel || newLabel === oldLabel) {
154
+ labelEl.textContent = oldLabel;
155
+ return;
156
+ }
157
+
158
+ fetch("/api/templates/rename", {
159
+ method: "POST",
160
+ headers: { "Content-Type": "application/json" },
161
+ body: JSON.stringify({ templateId, newLabel }),
162
+ })
163
+ .then((r) => r.json())
164
+ .then((data) => {
165
+ if (data.ok) {
166
+ labelEl.textContent = data.newLabel;
167
+ } else {
168
+ labelEl.textContent = oldLabel;
169
+ if (typeof showError === "function") showError(data.error || "Rename failed");
170
+ }
171
+ })
172
+ .catch(() => {
173
+ labelEl.textContent = oldLabel;
174
+ if (typeof showError === "function") showError("Rename failed");
175
+ });
176
+ }
177
+
178
+ labelEl.addEventListener("blur", commit, { once: true });
179
+ labelEl.addEventListener("keydown", function handler(e) {
180
+ if (e.key === "Enter") {
181
+ e.preventDefault();
182
+ labelEl.removeEventListener("keydown", handler);
183
+ labelEl.blur();
184
+ }
185
+ if (e.key === "Escape") {
186
+ labelEl.textContent = oldLabel;
187
+ labelEl.removeEventListener("keydown", handler);
188
+ labelEl.blur();
189
+ }
111
190
  });
112
191
  }
113
192
 
@@ -389,6 +468,111 @@ document.getElementById("brand-upload-brandvoice").querySelector("input").addEve
389
468
  if (e.target.files[0]) handleBrandFileSelected("brandvoice", e.target.files[0]);
390
469
  });
391
470
 
471
+ // Dashboard theme heading — double-click to rename
472
+ document.getElementById("dashboard-theme-heading")?.addEventListener("dblclick", () => {
473
+ const el = document.getElementById("dashboard-theme-heading");
474
+ if (!el || !currentDashboardSessionId) return;
475
+ if (el.contentEditable === "true") return;
476
+
477
+ const oldName = el.textContent.trim();
478
+ el.contentEditable = "true";
479
+ el.classList.add("dashboard__theme-heading--editing");
480
+ el.focus();
481
+
482
+ const range = document.createRange();
483
+ range.selectNodeContents(el);
484
+ const sel = window.getSelection();
485
+ sel.removeAllRanges();
486
+ sel.addRange(range);
487
+
488
+ function commit() {
489
+ el.contentEditable = "false";
490
+ el.classList.remove("dashboard__theme-heading--editing");
491
+
492
+ const newName = el.textContent.trim();
493
+ if (!newName || newName === oldName) {
494
+ el.textContent = oldName;
495
+ return;
496
+ }
497
+
498
+ fetch("/api/themes/rename", {
499
+ method: "POST",
500
+ headers: { "Content-Type": "application/json" },
501
+ body: JSON.stringify({ sessionId: currentDashboardSessionId, newName }),
502
+ })
503
+ .then((r) => r.json())
504
+ .then((data) => {
505
+ if (data.ok) {
506
+ el.textContent = data.newName;
507
+ currentDashboardTheme = data.newName;
508
+ document.getElementById("dashboard-theme-name").textContent = data.newName;
509
+ window.location.hash = "#/dashboard/" + encodeURIComponent(data.newName);
510
+ // Update rail
511
+ const railItem = document.querySelector(`.project-rail__item[data-name="${oldName}"]`);
512
+ if (railItem) {
513
+ railItem.dataset.name = data.newName;
514
+ const nameSpan = railItem.querySelector(".project-rail__item-name");
515
+ if (nameSpan) nameSpan.textContent = data.newName;
516
+ const bubble = railItem.querySelector(".project-rail__item-bubble");
517
+ if (bubble) bubble.textContent = data.newName.charAt(0).toUpperCase();
518
+ }
519
+ if (typeof updateRailActive === "function") updateRailActive();
520
+ } else {
521
+ el.textContent = oldName;
522
+ if (typeof showError === "function") showError(data.error || "Rename failed");
523
+ }
524
+ })
525
+ .catch(() => {
526
+ el.textContent = oldName;
527
+ if (typeof showError === "function") showError("Rename failed");
528
+ });
529
+ }
530
+
531
+ el.addEventListener("blur", commit, { once: true });
532
+ el.addEventListener("keydown", function handler(e) {
533
+ if (e.key === "Enter") {
534
+ e.preventDefault();
535
+ el.removeEventListener("keydown", handler);
536
+ el.blur();
537
+ }
538
+ if (e.key === "Escape") {
539
+ el.textContent = oldName;
540
+ el.removeEventListener("keydown", handler);
541
+ el.blur();
542
+ }
543
+ });
544
+ });
545
+
546
+ // Download ZIP button
547
+ document.getElementById("dashboard-download-zip").addEventListener("click", async () => {
548
+ const btn = document.getElementById("dashboard-download-zip");
549
+ const origHTML = btn.innerHTML;
550
+ btn.disabled = true;
551
+ btn.querySelector("span").textContent = "Downloading...";
552
+
553
+ try {
554
+ const res = await fetch("/api/download-zip");
555
+ if (!res.ok) {
556
+ const err = await res.json().catch(() => ({ error: "Download failed" }));
557
+ throw new Error(err.error || "Download failed");
558
+ }
559
+ const blob = await res.blob();
560
+ const url = URL.createObjectURL(blob);
561
+ const a = document.createElement("a");
562
+ a.href = url;
563
+ a.download = (currentDashboardTheme || "theme") + ".zip";
564
+ document.body.appendChild(a);
565
+ a.click();
566
+ a.remove();
567
+ URL.revokeObjectURL(url);
568
+ } catch (err) {
569
+ if (typeof vibeAlert === "function") vibeAlert(err.message, "Error");
570
+ } finally {
571
+ btn.disabled = false;
572
+ btn.innerHTML = origHTML;
573
+ }
574
+ });
575
+
392
576
  // Humanify toggle
393
577
  const humanifyCheckbox = document.getElementById("humanify-checkbox");
394
578
  if (humanifyCheckbox) {
package/ui/dialog.js CHANGED
@@ -6,10 +6,9 @@
6
6
  // HTML-escape helper (standalone so dialog.js has no load-order dependency)
7
7
  if (typeof esc === "undefined") {
8
8
  // eslint-disable-next-line no-var
9
+ var _escMap = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
9
10
  var esc = function (str) {
10
- const el = document.createElement("span");
11
- el.textContent = String(str);
12
- return el.innerHTML;
11
+ return String(str).replace(/[&<>"']/g, function (c) { return _escMap[c]; });
13
12
  };
14
13
  }
15
14
 
package/ui/favicon.ico CHANGED
Binary file
@@ -38,6 +38,7 @@ async function openFieldEditor(moduleName) {
38
38
  }
39
39
 
40
40
  function closeFieldEditor() {
41
+ if (updateTimer) { clearTimeout(updateTimer); updateTimer = null; }
41
42
  currentEditModule = null;
42
43
  if (typeof showModuleListView === "function") {
43
44
  showModuleListView();
package/ui/index.html CHANGED
@@ -41,7 +41,7 @@
41
41
  <!-- ============================================================ -->
42
42
  <div class="setup-topbar" id="setup-topbar">
43
43
  <div class="topbar__brand">
44
- <div class="topbar__logo-icon">v</div>
44
+ <div class="topbar__logo-icon"><svg viewBox="0 0 512 512" width="18" height="18"><path d="M256 76 Q280 220 436 256 Q280 292 256 436 Q232 292 76 256 Q232 220 256 76 Z" fill="currentColor"/></svg></div>
45
45
  <span class="topbar__brand-name">vibeSpot</span>
46
46
  </div>
47
47
  <div style="margin-left:auto">
@@ -140,7 +140,7 @@
140
140
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
141
141
  </button>
142
142
  <div class="topbar__brand">
143
- <div class="topbar__logo-icon">v</div>
143
+ <div class="topbar__logo-icon"><svg viewBox="0 0 512 512" width="18" height="18"><path d="M256 76 Q280 220 436 256 Q280 292 256 436 Q232 292 76 256 Q232 220 256 76 Z" fill="currentColor"/></svg></div>
144
144
  <span class="topbar__brand-name">vibeSpot</span>
145
145
  </div>
146
146
  <span class="topbar__project-pill" id="dashboard-theme-name"></span>
@@ -163,6 +163,17 @@
163
163
  <div class="dashboard__body">
164
164
  <div class="dashboard__container">
165
165
 
166
+ <h1 class="dashboard__theme-heading" id="dashboard-theme-heading"></h1>
167
+
168
+ <div class="dashboard__theme-path" id="dashboard-theme-path">
169
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="flex-shrink:0"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
170
+ <span class="dashboard__theme-path-text" id="dashboard-theme-path-text"></span>
171
+ <button class="dashboard__download-btn" id="dashboard-download-zip" title="Download theme as ZIP">
172
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
173
+ <span>Download ZIP</span>
174
+ </button>
175
+ </div>
176
+
166
177
  <!-- Brand Assets -->
167
178
  <section class="dashboard__section">
168
179
  <div class="dashboard__section-header">
@@ -267,10 +278,9 @@
267
278
  <div class="app hidden" id="app-screen">
268
279
  <header class="topbar">
269
280
  <div class="topbar__left">
270
- <div class="topbar__brand">
271
- <div class="topbar__logo-icon">v</div>
272
- <span class="topbar__brand-name">vibeSpot</span>
273
- </div>
281
+ <button class="topbar__back-btn" id="app-back" title="Back to theme overview">
282
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
283
+ </button>
274
284
  <span class="topbar__project-pill" id="theme-name"></span>
275
285
  </div>
276
286
  <div class="topbar__center">
@@ -294,9 +304,6 @@
294
304
  <button class="topbar__icon-btn" id="btn-history" title="Version History" style="display:none">
295
305
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
296
306
  </button>
297
- <button class="topbar__icon-btn" id="btn-settings" title="Settings">
298
- <svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M7.5 1.5h3l.4 2.1a5.5 5.5 0 0 1 1.3.7l2-.8 1.5 2.6-1.6 1.3a5.5 5.5 0 0 1 0 1.5l1.6 1.3-1.5 2.6-2-.8a5.5 5.5 0 0 1-1.3.7l-.4 2.1h-3l-.4-2.1a5.5 5.5 0 0 1-1.3-.7l-2 .8-1.5-2.6 1.6-1.3a5.5 5.5 0 0 1 0-1.5L2.3 6.1l1.5-2.6 2 .8a5.5 5.5 0 0 1 1.3-.7L7.5 1.5Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="9" cy="9" r="2" stroke="currentColor" stroke-width="1.5"/></svg>
299
- </button>
300
307
  <button class="btn btn--primary" id="btn-upload" title="Deploy theme to HubSpot">
301
308
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px;vertical-align:-2px"><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>
302
309
  Deploy