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/dashboard.js CHANGED
@@ -10,14 +10,14 @@ const PAGE_TYPE_LABELS = {
10
10
  landing_page: "LP",
11
11
  blog_post: "Blog",
12
12
  website_page: "Web",
13
- module_only: "Mod",
13
+ module_only: "Sec",
14
14
  };
15
15
 
16
16
  const PAGE_TYPE_FULL_LABELS = {
17
17
  landing_page: "Landing Page",
18
18
  blog_post: "Blog Post",
19
19
  website_page: "Website Page",
20
- module_only: "Module Only",
20
+ module_only: "Section Only",
21
21
  };
22
22
 
23
23
  // ---------------------------------------------------------------------------
@@ -26,6 +26,7 @@ const PAGE_TYPE_FULL_LABELS = {
26
26
 
27
27
  let currentDashboardTheme = "";
28
28
  let currentDashboardSessionId = "";
29
+ let currentDashboardIsImported = false;
29
30
 
30
31
  async function showDashboard(themeName) {
31
32
  currentDashboardTheme = themeName;
@@ -46,7 +47,11 @@ async function showDashboard(themeName) {
46
47
  const themesRes = await fetch("/api/themes");
47
48
  const themesData = await themesRes.json();
48
49
  currentDashboardSessionId = themesData.activeTheme?.id || "";
49
- } catch { currentDashboardSessionId = ""; }
50
+ currentDashboardIsImported = !!themesData.activeTheme?.isImported;
51
+ } catch {
52
+ currentDashboardSessionId = "";
53
+ currentDashboardIsImported = false;
54
+ }
50
55
 
51
56
  // Update URL
52
57
  const target = "#/dashboard/" + encodeURIComponent(themeName);
@@ -61,6 +66,7 @@ async function showDashboard(themeName) {
61
66
  function hideDashboard() {
62
67
  dashboardScreen.classList.add("hidden");
63
68
  currentDashboardTheme = "";
69
+ currentDashboardIsImported = false;
64
70
  closeModulePreview();
65
71
  }
66
72
 
@@ -82,11 +88,187 @@ async function refreshDashboard() {
82
88
  if (data.themePath) {
83
89
  document.getElementById("dashboard-theme-path-text").textContent = data.themePath;
84
90
  }
91
+ if (currentDashboardIsImported) {
92
+ await refreshInverseAnalysis();
93
+ } else {
94
+ hideInverseAnalysis();
95
+ }
85
96
  } catch (err) {
86
97
  console.error("Failed to load dashboard:", err);
87
98
  }
88
99
  }
89
100
 
101
+ // ---------------------------------------------------------------------------
102
+ // Import analysis
103
+ // ---------------------------------------------------------------------------
104
+
105
+ function hideInverseAnalysis() {
106
+ const section = document.getElementById("dashboard-inverse-section");
107
+ const summaryEl = document.getElementById("inverse-summary");
108
+ const status = document.getElementById("inverse-status");
109
+ const applyBtn = document.getElementById("btn-inverse-apply-tokens");
110
+ section?.classList.add("hidden");
111
+ if (summaryEl) summaryEl.innerHTML = "";
112
+ if (status) status.textContent = "Analyzing theme...";
113
+ if (applyBtn) applyBtn.classList.add("hidden");
114
+ }
115
+
116
+ async function refreshInverseAnalysis() {
117
+ const section = document.getElementById("dashboard-inverse-section");
118
+ const status = document.getElementById("inverse-status");
119
+ const summaryEl = document.getElementById("inverse-summary");
120
+ const applyBtn = document.getElementById("btn-inverse-apply-tokens");
121
+ if (!section || !summaryEl) return;
122
+
123
+ const capturedSessionId = currentDashboardSessionId;
124
+
125
+ try {
126
+ const res = await fetch("/api/inverse/analyze");
127
+ if (currentDashboardSessionId !== capturedSessionId) return;
128
+ const data = await res.json();
129
+ if (!res.ok || data.error) {
130
+ section.classList.add("hidden");
131
+ return;
132
+ }
133
+ section.classList.remove("hidden");
134
+ renderInverseAnalysis(data.report);
135
+ } catch (err) {
136
+ console.warn("Import analysis failed:", err);
137
+ section.classList.add("hidden");
138
+ }
139
+ }
140
+
141
+ function renderInverseAnalysis(report) {
142
+ const status = document.getElementById("inverse-status");
143
+ const summaryEl = document.getElementById("inverse-summary");
144
+ const applyBtn = document.getElementById("btn-inverse-apply-tokens");
145
+ if (!report || !summaryEl) return;
146
+
147
+ const counts = report.summary || {};
148
+ const tokens = report.designTokens || {};
149
+ const findings = report.findings || [];
150
+ const warnings = findings.filter((f) => f.severity === "warning").length;
151
+ const errors = findings.filter((f) => f.severity === "error").length;
152
+ const hasInferredTokens = (tokens.palette || []).length > 0;
153
+ const hasCssVars = (counts.cssVarCount || 0) > 0;
154
+
155
+ if (status) {
156
+ if (errors > 0) status.textContent = `${errors} issue${errors === 1 ? "" : "s"} need attention`;
157
+ else if (warnings > 0) status.textContent = `${warnings} warning${warnings === 1 ? "" : "s"}`;
158
+ else status.textContent = "No blocking risks found";
159
+ }
160
+
161
+ if (applyBtn) {
162
+ applyBtn.classList.toggle("hidden", hasCssVars || !hasInferredTokens);
163
+ applyBtn.disabled = false;
164
+ applyBtn.textContent = "Apply Tokens";
165
+ }
166
+
167
+ const stats = [
168
+ ["Modules (disk)", counts.moduleCount || 0],
169
+ ["Templates (disk)", counts.templateCount || 0],
170
+ ["Orphans", counts.orphanCount || 0],
171
+ ["Palette", counts.paletteSize || 0],
172
+ ["CSS Vars", counts.cssVarCount || 0],
173
+ ["Macros", counts.customMacroCount || 0],
174
+ ];
175
+
176
+ let html = `<div class="inverse-summary__stats">`;
177
+ for (const [label, value] of stats) {
178
+ html += `
179
+ <div class="inverse-stat">
180
+ <span class="inverse-stat__value">${esc(String(value))}</span>
181
+ <span class="inverse-stat__label">${esc(label)}</span>
182
+ </div>
183
+ `;
184
+ }
185
+ html += `</div>`;
186
+
187
+ if ((tokens.palette || []).length > 0) {
188
+ html += `<div class="inverse-block"><div class="inverse-block__label">Palette</div><div class="inverse-swatches">`;
189
+ for (const color of tokens.palette.slice(0, 8)) {
190
+ const label = color.varName ? `${color.value} (${color.varName})` : color.value;
191
+ html += `<span class="inverse-swatch" style="background:${inverseCssColor(color.value)}" title="${inverseEscAttr(label)}"></span>`;
192
+ }
193
+ html += `</div></div>`;
194
+ }
195
+
196
+ if ((tokens.fontFamilies || []).length > 0) {
197
+ html += `<div class="inverse-block"><div class="inverse-block__label">Typography</div><div class="inverse-tags">`;
198
+ for (const font of tokens.fontFamilies.slice(0, 4)) {
199
+ html += `<span class="inverse-tag">${esc(font)}</span>`;
200
+ }
201
+ html += `</div></div>`;
202
+ }
203
+
204
+ html += renderInverseFindings(findings);
205
+ summaryEl.innerHTML = html;
206
+ }
207
+
208
+ function renderInverseFindings(findings) {
209
+ if (!findings || findings.length === 0) {
210
+ return `<div class="inverse-findings inverse-findings--empty">No findings. This imported theme looks straightforward to edit.</div>`;
211
+ }
212
+
213
+ const severityOrder = { error: 0, warning: 1, info: 2 };
214
+ const sorted = [...findings].sort((a, b) => (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2));
215
+ const visible = sorted.slice(0, 5);
216
+ let html = `<div class="inverse-findings">`;
217
+ for (const finding of visible) {
218
+ const severity = ["error", "warning", "info"].includes(finding.severity) ? finding.severity : "info";
219
+ const fixAttr = finding.fix ? ` title="${inverseEscAttr(finding.fix)}"` : "";
220
+ html += `
221
+ <div class="inverse-finding inverse-finding--${severity}">
222
+ <span class="inverse-finding__severity">${esc(severity)}</span>
223
+ <span class="inverse-finding__message"${fixAttr}>${esc(finding.message)}</span>
224
+ </div>
225
+ `;
226
+ }
227
+ if (findings.length > visible.length) {
228
+ html += `<div class="inverse-findings__more">${findings.length - visible.length} more finding${findings.length - visible.length === 1 ? "" : "s"} available in the CLI report.</div>`;
229
+ }
230
+ html += `</div>`;
231
+ return html;
232
+ }
233
+
234
+ function inverseEscAttr(value) {
235
+ return esc(String(value)).replace(/"/g, "&quot;").replace(/'/g, "&#39;");
236
+ }
237
+
238
+ function inverseCssColor(value) {
239
+ const color = String(value || "").trim();
240
+ if (/^#[0-9a-fA-F]{3,8}$/.test(color)) return color;
241
+ if (/^rgba?\([0-9.,%\s]+\)$/.test(color)) return color;
242
+ if (/^hsla?\([0-9.,%\sdegturnrad+-]+\)$/.test(color)) return color;
243
+ return "transparent";
244
+ }
245
+
246
+ document.getElementById("btn-inverse-apply-tokens")?.addEventListener("click", async () => {
247
+ const btn = document.getElementById("btn-inverse-apply-tokens");
248
+ if (!btn) return;
249
+
250
+ btn.disabled = true;
251
+ btn.textContent = "Applying...";
252
+ try {
253
+ const res = await fetch("/api/inverse/apply-tokens", { method: "POST" });
254
+ const data = await res.json();
255
+ if (!res.ok || data.error) {
256
+ await vibeAlert(data.error || "Failed to apply tokens.", "Error");
257
+ btn.disabled = false;
258
+ btn.textContent = "Apply Tokens";
259
+ return;
260
+ }
261
+ if (!data.applied) {
262
+ await vibeAlert(data.reason || "No tokens were applied.", "Info");
263
+ }
264
+ await refreshInverseAnalysis();
265
+ } catch (err) {
266
+ await vibeAlert("Failed to apply tokens: " + err.message, "Error");
267
+ btn.disabled = false;
268
+ btn.textContent = "Apply Tokens";
269
+ }
270
+ });
271
+
90
272
  // ---------------------------------------------------------------------------
91
273
  // Template list
92
274
  // ---------------------------------------------------------------------------
@@ -108,7 +290,7 @@ function renderTemplateList(templates) {
108
290
  item.innerHTML = `
109
291
  <span class="dashboard__template-badge dashboard__template-badge--${tpl.pageType}">${esc(PAGE_TYPE_LABELS[tpl.pageType] || "?")}</span>
110
292
  <span class="dashboard__template-label">${esc(tpl.label)}</span>
111
- <span class="dashboard__template-meta">${tpl.moduleCount} module${tpl.moduleCount !== 1 ? "s" : ""}</span>
293
+ <span class="dashboard__template-meta">${tpl.moduleCount} section${tpl.moduleCount !== 1 ? "s" : ""}</span>
112
294
  <button class="btn btn--sm btn--primary dashboard__template-open" data-id="${esc(tpl.id)}">Open</button>
113
295
  <button class="dashboard__template-clone" data-id="${esc(tpl.id)}" title="Clone template">&#x29C9;</button>
114
296
  <button class="dashboard__template-delete" data-id="${esc(tpl.id)}" title="Delete template">&times;</button>
@@ -203,7 +385,7 @@ function renderModuleLibrary(modules) {
203
385
  const container = document.getElementById("dashboard-module-library");
204
386
 
205
387
  if (modules.length === 0) {
206
- container.innerHTML = `<p class="dashboard__empty-state">Modules will appear here as you build pages.</p>`;
388
+ container.innerHTML = `<p class="dashboard__empty-state">Sections will appear here as you build pages.</p>`;
207
389
  closeModulePreview();
208
390
  return;
209
391
  }
@@ -275,7 +457,7 @@ document.getElementById("dashboard-preview-delete").addEventListener("click", as
275
457
  if (!moduleName) return;
276
458
 
277
459
  const ok = await vibeConfirm(
278
- `Delete module "${moduleName}"?`,
460
+ `Delete section "${moduleName}"?`,
279
461
  "This will remove it from all templates and delete it from disk.",
280
462
  { confirmLabel: "Delete" }
281
463
  );
@@ -290,7 +472,7 @@ document.getElementById("dashboard-preview-delete").addEventListener("click", as
290
472
  closeModulePreview();
291
473
  await refreshDashboard();
292
474
  } catch (err) {
293
- await vibeAlert("Failed to delete module: " + err.message, "Error");
475
+ await vibeAlert("Failed to delete section: " + err.message, "Error");
294
476
  }
295
477
  });
296
478
 
@@ -385,7 +567,7 @@ async function extractBrandAsset(type, card) {
385
567
  );
386
568
  if (view) await vibeViewContent(data.content, ASSET_LABELS[type], ASSET_FILES[type]);
387
569
  } else {
388
- await vibeAlert(data.error || "Nothing to extract — generate some modules first.", "Info");
570
+ await vibeAlert(data.error || "Nothing to extract — generate some sections first.", "Info");
389
571
  }
390
572
  } catch (err) {
391
573
  await vibeAlert("Extraction failed: " + err.message, "Error");
@@ -425,7 +607,7 @@ async function createTemplateFromPageType(pageType) {
425
607
  landing_page: "Landing Page",
426
608
  blog_post: "Blog Post",
427
609
  website_page: "Website Page",
428
- module_only: "Module",
610
+ module_only: "Section",
429
611
  };
430
612
 
431
613
  const label = await vibePrompt("Template name", defaultLabels[pageType] || "New Template");
@@ -502,8 +684,8 @@ function vibeDeleteTemplateDialog() {
502
684
  <div class="confirm-dialog__title">Delete template?</div>
503
685
  <p class="confirm-dialog__warn">This cannot be undone.</p>
504
686
  <div class="confirm-dialog__actions" style="flex-direction:column;gap:8px">
505
- <button class="btn btn--danger" data-action="with_modules" style="width:100%">Delete template and its modules</button>
506
- <button class="btn btn--secondary" data-action="template_only" style="width:100%">Delete template only (keep modules)</button>
687
+ <button class="btn btn--danger" data-action="with_modules" style="width:100%">Delete template and its sections</button>
688
+ <button class="btn btn--secondary" data-action="template_only" style="width:100%">Delete template only (keep sections)</button>
507
689
  <button class="btn btn--secondary" data-action="cancel" style="width:100%">Cancel</button>
508
690
  </div>
509
691
  </div>
@@ -666,7 +848,7 @@ document.getElementById("btn-extract-all")?.addEventListener("click", async () =
666
848
  if (names.length > 0) {
667
849
  await vibeAlert(`Extracted: ${names.join(", ")}`, "Done");
668
850
  } else {
669
- await vibeAlert("Nothing to extract \u2014 generate some modules first.", "Info");
851
+ await vibeAlert("Nothing to extract \u2014 generate some sections first.", "Info");
670
852
  }
671
853
  } else {
672
854
  await vibeAlert(data.error || "Extraction failed", "Error");
@@ -21,7 +21,7 @@
21
21
  </a>
22
22
  <div class="doc-topbar__search">
23
23
  <span class="doc-topbar__search-icon">&#128269;</span>
24
- <input type="text" id="doc-search-input" placeholder="Search docs..." autocomplete="off">
24
+ <input type="text" id="doc-search-input" placeholder="Search docs..." autocomplete="off" aria-label="Search documentation">
25
25
  <span class="doc-topbar__shortcut">/</span>
26
26
  </div>
27
27
  <a href="/" class="doc-topbar__back">&larr; Back to App</a>
@@ -97,6 +97,8 @@
97
97
  <a class="doc-nav__link" href="#deploying">Deploying to HubSpot</a>
98
98
  <a class="doc-nav__link doc-nav__link--sub" href="#auto-fix">Auto-Fix</a>
99
99
  <a class="doc-nav__link doc-nav__link--sub" href="#creating-page-hubspot">Creating a Page</a>
100
+ <a class="doc-nav__link" href="#zip-download">ZIP Download</a>
101
+ <a class="doc-nav__link" href="#marketplace">Marketplace</a>
100
102
  <a class="doc-nav__link" href="#version-history">Version History</a>
101
103
  </div>
102
104
 
@@ -105,6 +107,7 @@
105
107
  <a class="doc-nav__link" href="#settings">Settings</a>
106
108
  <a class="doc-nav__link doc-nav__link--sub" href="#settings-ai">AI Tab</a>
107
109
  <a class="doc-nav__link doc-nav__link--sub" href="#settings-hubspot">HubSpot Tab</a>
110
+ <a class="doc-nav__link doc-nav__link--sub" href="#settings-figma">Figma Tab</a>
108
111
  <a class="doc-nav__link" href="#cli-commands">CLI Commands</a>
109
112
  <a class="doc-nav__link" href="#shortcuts">Shortcuts</a>
110
113
  <a class="doc-nav__link" href="#troubleshooting">Troubleshooting</a>
@@ -205,7 +208,7 @@ vibespot</code></pre>
205
208
  <tr><td>Claude Code</td><td>CLI</td><td>Install <code>claude</code> CLI, authenticate</td><td>Included in Claude subscription</td><td>Subscribers who want zero API costs</td></tr>
206
209
  <tr><td>Anthropic API</td><td>API</td><td>Add <code>ANTHROPIC_API_KEY</code></td><td>Pay-per-token</td><td>Fastest structured output, reliable</td></tr>
207
210
  <tr><td>Claude OAuth</td><td>API</td><td>Run <code>claude setup-token</code>, paste token</td><td>Included in Claude subscription</td><td>Subscribers without API budget</td></tr>
208
- <tr><td>OpenAI API</td><td>API</td><td>Add <code>OPENAI_API_KEY</code></td><td>Pay-per-token</td><td>GPT-4o users, existing OpenAI budget</td></tr>
211
+ <tr><td>OpenAI API</td><td>API</td><td>Add <code>OPENAI_API_KEY</code></td><td>Pay-per-token</td><td>GPT-5.5 users, existing OpenAI budget</td></tr>
209
212
  <tr><td>Gemini API</td><td>API</td><td>Add <code>GEMINI_API_KEY</code></td><td>Free tier available</td><td>Cost-conscious users, free quota</td></tr>
210
213
  <tr><td>Gemini CLI</td><td>CLI</td><td>Install <code>gemini</code> CLI, authenticate</td><td>Free (with Google account)</td><td>Free option without API keys</td></tr>
211
214
  <tr><td>Codex CLI</td><td>CLI</td><td>Install <code>codex</code> CLI, authenticate</td><td>Included in OpenAI subscription</td><td>OpenAI subscribers</td></tr>
@@ -251,7 +254,7 @@ vibespot</code></pre>
251
254
  </div>
252
255
  </div>
253
256
  <div class="doc-tabs__panel" id="tab-openai">
254
- <p>Use OpenAI's GPT-4o or any compatible model through the OpenAI API.</p>
257
+ <p>Use OpenAI's GPT-5.5 or any compatible model through the OpenAI API.</p>
255
258
  <ol class="doc-steps">
256
259
  <li>Get an API key from <a href="https://platform.openai.com/api-keys" target="_blank">platform.openai.com</a>.</li>
257
260
  <li>Paste the key in Settings &rarr; AI tab &rarr; OpenAI API Key, or set <code>export OPENAI_API_KEY=sk-...</code></li>
@@ -313,7 +316,7 @@ vibespot</code></pre>
313
316
  <tr><td><code>geminiApiKey</code></td><td>string</td><td>Google AI / Gemini API key</td></tr>
314
317
  <tr><td><code>claudeCodeModel</code></td><td>string</td><td>Model override for Claude Code CLI</td></tr>
315
318
  <tr><td><code>anthropicApiModel</code></td><td>string</td><td>Anthropic API model (default: <code>claude-sonnet-4-20250514</code>)</td></tr>
316
- <tr><td><code>openaiApiModel</code></td><td>string</td><td>OpenAI API model (default: <code>gpt-4o</code>)</td></tr>
319
+ <tr><td><code>openaiApiModel</code></td><td>string</td><td>OpenAI API model (default: <code>gpt-5.5</code>)</td></tr>
317
320
  <tr><td><code>hubspotAccounts</code></td><td>array</td><td>List of HubSpot accounts with PAK, portal ID, and data center</td></tr>
318
321
  <tr><td><code>activeHubSpotAccount</code></td><td>string</td><td>Portal ID of the currently active HubSpot account</td></tr>
319
322
  <tr><td><code>hubspotUploadMode</code></td><td>string</td><td><code>"api"</code> (default) or <code>"cli"</code></td></tr>
@@ -424,7 +427,7 @@ vibespot</code></pre>
424
427
  <div class="mock-editor__resize" title="Drag to resize panels"></div>
425
428
  <div class="mock-editor__right">
426
429
  <div class="mock-preview">
427
- <div class="mock-preview__tabs"><span class="mock-preview__tab active">Preview</span><span class="mock-preview__tab">Code</span></div>
430
+ <div class="mock-preview__tabs"><span class="mock-preview__tab active">Preview</span><span class="mock-preview__tab">Plan</span><span class="mock-preview__tab">Code</span></div>
428
431
  <div class="mock-preview__chrome">
429
432
  <div class="mock-preview__dots"><span class="mock-preview__dot"></span><span class="mock-preview__dot"></span><span class="mock-preview__dot"></span></div>
430
433
  <div class="mock-preview__url">my-startup.vibespot.app</div>
@@ -500,7 +503,7 @@ vibespot</code></pre>
500
503
  <li><code>fields.json</code> &mdash; Field definitions for the HubSpot content editor</li>
501
504
  </ul>
502
505
  <p>At the theme level, you also have access to <code>shared.css</code> (design system utilities) and <code>shared.js</code> (scroll animations, shared behaviors).</p>
503
- <p>The editor uses CodeMirror 6 with syntax highlighting for HTML, CSS, JavaScript, and JSON. Save your changes with <kbd>Cmd</kbd>+<kbd>S</kbd> (or <kbd>Ctrl</kbd>+<kbd>S</kbd> on Windows/Linux) to write to disk and refresh the preview.</p>
506
+ <p>The editor uses CodeMirror 6 with syntax highlighting for HTML, CSS, JavaScript, and JSON. Save your changes with <kbd>Cmd</kbd>+<kbd>S</kbd> (or <kbd>Ctrl</kbd>+<kbd>S</kbd> on Windows/Linux) or click the <strong>Save</strong> button to write to disk and refresh the preview.</p>
504
507
 
505
508
  <h3 id="module-sidebar">Module Sidebar</h3>
506
509
  <p>The module sidebar is a <strong>toggleable slideout</strong> panel. Click the <strong>Modules</strong> button (grid icon + count badge) at the top of the left panel to open it. The slideout slides in from the left over the chat area. Click the <strong>&times;</strong> button or the Modules button again to close it.</p>
@@ -760,6 +763,9 @@ vibespot</code></pre>
760
763
  <li><strong>Refine.</strong> Subsequent messages tweak the plan. "Move the FAQ above the pricing." "Change the primary CTA to 'Book a demo'." The AI confirms what changed in chat and updates the plan in the pane. It only asks clarifying questions when your edit creates a new ambiguity.</li>
761
764
  </ol>
762
765
 
766
+ <h4>Plan-mode templates (skip Understand)</h4>
767
+ <p>For common page types, the cold-start <em>Understand</em> phase is unnecessary. When the Plan pane is empty, vibeSpot shows a picker of pre-canned templates &mdash; SaaS landing, e-commerce product, event registration, blog/content hub, portfolio, agency/services, restaurant. Picking one seeds <code>.vibespot/plan.md</code> with goal, audience, primary CTA, suggested kebab-case modules, brand/tone, and a tailored "Open questions" list. The next AI turn skips straight to those page-specific questions instead of asking generic ones. Pick <strong>Blank plan</strong> to keep the original free-form behavior.</p>
768
+
763
769
  <h3 id="plan-mode-pane">The Plan Pane &amp; Inline Editing</h3>
764
770
  <p>The <strong>Plan</strong> tab in the right pane (between Preview and Code) is the source of truth for the current plan. While plan mode is on, this tab auto-selects when you send a message so the latest plan is always visible.</p>
765
771
  <p>Two modes inside the pane:</p>
@@ -990,6 +996,60 @@ vibespot</code></pre>
990
996
  </ul>
991
997
  </div>
992
998
 
999
+ <!-- ============================================================
1000
+ Section: ZIP Download
1001
+ ============================================================ -->
1002
+ <div class="doc-section" id="zip-download">
1003
+ <h2 id="zip-download-heading">ZIP Download <a href="#zip-download" class="doc-anchor">#</a></h2>
1004
+ <p>Export your theme as a ZIP archive for offline use, sharing, or manual upload to HubSpot.</p>
1005
+ <p>Click the <strong>Download ZIP</strong> button on the dashboard toolbar (next to the theme name) to generate and download a <code>.zip</code> file containing all theme files: modules, templates, shared CSS/JS, and assets. The ZIP file is named after the theme (e.g., <code>my-saas-landing.zip</code>).</p>
1006
+ <div class="doc-callout doc-callout--tip">
1007
+ <div class="doc-callout__label">&#10024; When to use ZIP download</div>
1008
+ <p>ZIP download is useful when you want to share the theme with a colleague who does not have vibeSpot, back up the theme independently of git, or upload manually through HubSpot's Design Manager.</p>
1009
+ </div>
1010
+ </div>
1011
+
1012
+ <!-- ============================================================
1013
+ Section: HubSpot Marketplace publication path
1014
+ ============================================================ -->
1015
+ <div class="doc-section" id="marketplace">
1016
+ <h2 id="marketplace-heading">HubSpot Marketplace <a href="#marketplace" class="doc-anchor">#</a></h2>
1017
+ <p>The HubSpot Marketplace has stricter requirements than a private theme upload. vibeSpot ships a Marketplace publication path that audits a generated theme and helps you collect the listing metadata you'll paste into HubSpot's submission form.</p>
1018
+
1019
+ <h3 id="marketplace-button">Where to find it</h3>
1020
+ <p>In the editor topbar, click the storefront icon (next to the version history clock) to open the Marketplace panel. From the CLI, run <code>vibespot marketplace check</code>.</p>
1021
+
1022
+ <h3 id="marketplace-validator">What gets validated</h3>
1023
+ <ul>
1024
+ <li><strong>theme.json</strong> — HubSpot's required fields (<code>label</code>, <code>preview_path</code>, <code>screenshot_path</code>, <code>version</code>, <code>documentation_url</code>, <code>license</code>, <code>example_url</code>, <code>enable_domain_stylesheets</code>, <code>is_available_for_new_content</code>) and the <code>author</code> block (<code>name</code>, <code>email</code>, <code>url</code>).</li>
1025
+ <li><strong>Modules</strong> — every <code>meta.json</code> needs a <code>label</code>; every field needs a <code>label</code>; <code>help_text</code> is recommended.</li>
1026
+ <li><strong>Screenshot</strong> — the path declared in <code>theme.json</code> must point to an existing PNG/JPG.</li>
1027
+ <li><strong>No CDN imports</strong> — <code>@import url(https://…)</code>, <code>&lt;link href="https://…"&gt;</code>, <code>&lt;script src="https://…"&gt;</code> are flagged.</li>
1028
+ <li><strong>No portal-specific URLs</strong> — hard-coded <code>hubfs</code>, <code>hubspotusercontent</code>, or portal preview URLs are flagged.</li>
1029
+ <li><strong>Accessibility baseline</strong> — <code>&lt;img&gt;</code> tags need <code>alt</code>, modules should use semantic tags.</li>
1030
+ <li><strong>Listing metadata</strong> — a <code>marketplace.json</code> sidecar with category, description, features, support URL.</li>
1031
+ </ul>
1032
+
1033
+ <h3 id="marketplace-fixes">Auto-fixable findings</h3>
1034
+ <p>A subset of findings can be patched without user input — missing module labels and missing field labels (vibeSpot fills in human-readable defaults derived from the slug or field name), and external CDN <code>@import</code>/<code>&lt;link&gt;</code>/<code>&lt;script&gt;</code> references (stripped from CSS and module HTML). Click <strong>Apply fixes</strong> in the panel, or run <code>vibespot marketplace check --fix</code>.</p>
1035
+
1036
+ <h3 id="marketplace-listing">Listing metadata editor</h3>
1037
+ <p>Click <strong>Edit listing</strong> in the panel (or run <code>vibespot marketplace edit</code>) to fill in the values that go on the Marketplace listing page: category, description, features, support URL, documentation URL, pricing tier. The values are written to a <code>marketplace.json</code> sidecar at the theme root and are picked up by the validator.</p>
1038
+
1039
+ <h3 id="marketplace-submission">Submitting to HubSpot</h3>
1040
+ <p>vibeSpot does <strong>not</strong> submit the theme for you — that requires HubSpot Partner API access and a manual review process. Instead:</p>
1041
+ <ol>
1042
+ <li>Run the Marketplace check until it passes.</li>
1043
+ <li>Upload the theme to your developer portal with <strong>Deploy</strong> (or <code>vibespot upload</code>).</li>
1044
+ <li>In your HubSpot developer portal, open <em>Marketplace listings → Submit new listing</em> and paste the values from <code>marketplace.json</code>.</li>
1045
+ <li>HubSpot reviews the listing and the theme on their side; address any feedback they send back.</li>
1046
+ </ol>
1047
+ <div class="doc-callout doc-callout--tip">
1048
+ <div class="doc-callout__label">&#10024; Screenshots</div>
1049
+ <p>HubSpot expects a 1500&times;1000 PNG at <code>screenshot_path</code> plus additional listing images uploaded directly through the portal. vibeSpot validates that the file exists; capturing it is up to you for now.</p>
1050
+ </div>
1051
+ </div>
1052
+
993
1053
  <!-- ============================================================
994
1054
  Section 8: Version History
995
1055
  ============================================================ -->
@@ -1001,7 +1061,8 @@ vibespot</code></pre>
1001
1061
  <p>Each time the AI pipeline generates or modifies modules, vibeSpot creates a git commit in the theme's local repository. The commit message describes what changed (e.g., "Generated hero, features, pricing, footer" or "Modified hero: updated headline and background color"). No manual saving is required.</p>
1002
1062
 
1003
1063
  <h3 id="browsing-history">Browsing Versions</h3>
1004
- <p>Click the clock icon in the topbar to open the version history panel. You will see a timeline of all versions with:</p>
1064
+ <p>The compact <strong>history timeline</strong> sits above the chat input each pill is one generation step, labeled with the user message that produced it. The currently active version is highlighted. Hover any pill to see the commit hash, age, and the section names changed in that step.</p>
1065
+ <p>For the deeper view, click the clock icon in the topbar to open the full version history panel with:</p>
1005
1066
  <ul>
1006
1067
  <li>Timestamp for each version</li>
1007
1068
  <li>Description of what changed</li>
@@ -1009,11 +1070,14 @@ vibespot</code></pre>
1009
1070
  </ul>
1010
1071
 
1011
1072
  <h3 id="rolling-back">Rolling Back</h3>
1012
- <p>Click any version in the timeline to restore the theme to that point. This is a non-destructive operation: vibeSpot creates a new commit that reverts to the selected state, so you never lose the intermediate history. You can always go forward again to a newer version.</p>
1073
+ <p>Click any pill in the timeline (or any entry in the history panel) to restore the theme to that point. This is a non-destructive operation: vibeSpot creates a new commit that reverts to the selected state, so you never lose the intermediate history. You can always go forward again to a newer version.</p>
1074
+
1075
+ <h3 id="undo-redo-shortcuts">Keyboard Shortcuts</h3>
1076
+ <p>Press <kbd>Ctrl</kbd>+<kbd>Z</kbd> (<kbd>Cmd</kbd>+<kbd>Z</kbd> on macOS) to step back one version, or <kbd>Ctrl</kbd>+<kbd>Y</kbd> / <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> to step forward. Shortcuts are ignored while the chat input or any other text field is focused, and while the AI is generating.</p>
1013
1077
 
1014
1078
  <div class="doc-callout doc-callout--tip">
1015
1079
  <div class="doc-callout__label">&#10024; Tip</div>
1016
- <p>Use version history as an undo system. If an AI generation goes in the wrong direction, roll back to the previous version and try a different prompt.</p>
1080
+ <p>Use the timeline as a fast undo. If an AI generation goes in the wrong direction, hit Ctrl+Z to revert and try a different prompt.</p>
1017
1081
  </div>
1018
1082
  </div>
1019
1083
 
@@ -1022,7 +1086,7 @@ vibespot</code></pre>
1022
1086
  ============================================================ -->
1023
1087
  <div class="doc-section" id="settings">
1024
1088
  <h2 id="settings-heading">Settings Reference <a href="#settings" class="doc-anchor">#</a></h2>
1025
- <p>Open the Settings panel by clicking the gear icon in the topbar. Settings are organized into four tabs.</p>
1089
+ <p>Open the Settings panel by clicking the gear icon in the topbar. Settings are organized into five tabs.</p>
1026
1090
 
1027
1091
  <h3 id="settings-ai">AI Tab</h3>
1028
1092
 
@@ -1032,6 +1096,7 @@ vibespot</code></pre>
1032
1096
  <div class="mock-settings__tabs">
1033
1097
  <span class="mock-settings__tab active">AI</span>
1034
1098
  <span class="mock-settings__tab">HubSpot</span>
1099
+ <span class="mock-settings__tab">Figma</span>
1035
1100
  <span class="mock-settings__tab">GitHub</span>
1036
1101
  <span class="mock-settings__tab">vibeSpot</span>
1037
1102
  </div>
@@ -1139,6 +1204,18 @@ vibespot</code></pre>
1139
1204
  <li><strong>Switch/remove</strong> &mdash; Click an account to make it active. Click the remove button to disconnect it.</li>
1140
1205
  </ul>
1141
1206
 
1207
+ <h3 id="settings-figma">Figma Tab</h3>
1208
+ <p>The Figma tab manages authentication for the <a href="#figma-import">Figma Import</a> feature.</p>
1209
+ <ul>
1210
+ <li><strong>Figma Personal Access Token</strong> &mdash; Paste a token generated from your Figma account settings (Account Settings &rarr; Personal access tokens). The token needs <strong>File content (read-only)</strong> scope.</li>
1211
+ <li><strong>Test Connection</strong> &mdash; Validates the token against the Figma API to confirm it is working.</li>
1212
+ <li><strong>Token storage</strong> &mdash; The token is saved locally in <code>~/.vibespot/config.json</code> as <code>figmaToken</code> and is only sent to the Figma API.</li>
1213
+ </ul>
1214
+ <div class="doc-callout doc-callout--tip">
1215
+ <div class="doc-callout__label">&#10024; One-off tokens</div>
1216
+ <p>You can also enter a Figma token directly in the import dialog without saving it globally. This is useful for one-time imports or when using a shared machine.</p>
1217
+ </div>
1218
+
1142
1219
  <h3 id="settings-github">GitHub Tab</h3>
1143
1220
  <p>The GitHub tab configures authentication for source imports (used by the "From React" flow).</p>
1144
1221
  <ul>
@@ -1170,6 +1247,8 @@ vibespot</code></pre>
1170
1247
  <tr><td><code>vibespot init</code></td><td>Environment check &amp; setup</td></tr>
1171
1248
  <tr><td><code>vibespot convert</code></td><td>React-to-HubSpot conversion</td></tr>
1172
1249
  <tr><td><code>vibespot upload</code></td><td>Upload theme to HubSpot</td></tr>
1250
+ <tr><td><code>vibespot marketplace check</code></td><td>Audit a theme against HubSpot Marketplace requirements</td></tr>
1251
+ <tr><td><code>vibespot marketplace edit</code></td><td>Edit Marketplace listing metadata (<code>marketplace.json</code>)</td></tr>
1173
1252
  <tr><td><code>vibespot doctor</code></td><td>Diagnostics &amp; health check</td></tr>
1174
1253
  </tbody>
1175
1254
  </table>
@@ -26,7 +26,7 @@ async function openFieldEditor(moduleName) {
26
26
  const data = await res.json();
27
27
  const mod = data.modules.find((m) => m.moduleName === moduleName);
28
28
  if (!mod) {
29
- editorContent.innerHTML = "<p>Module not found</p>";
29
+ editorContent.innerHTML = "<p>Section not found</p>";
30
30
  return;
31
31
  }
32
32