vibespot 1.2.0 → 1.3.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.
Files changed (49) hide show
  1. package/README.md +54 -5
  2. package/assets/blog-rules.md +251 -0
  3. package/assets/email-rules.md +390 -0
  4. package/assets/humanify-guide.md +300 -101
  5. package/assets/plan-templates/blog-content-hub.md +18 -9
  6. package/assets/plan-templates/email-announcement.md +41 -0
  7. package/assets/plan-templates/email-event-invite.md +43 -0
  8. package/assets/plan-templates/email-newsletter.md +41 -0
  9. package/assets/plan-templates/email-re-engagement.md +42 -0
  10. package/assets/plan-templates/email-welcome.md +41 -0
  11. package/dist/index.js +1460 -387
  12. package/dist/index.js.map +1 -1
  13. package/package.json +5 -5
  14. package/starters/06-blog-content-hub.json +75 -0
  15. package/starters/06-email-welcome.json +60 -0
  16. package/starters/07-email-announcement.json +60 -0
  17. package/starters/08-email-newsletter.json +52 -0
  18. package/ui/chat.js +777 -63
  19. package/ui/code-editor.js +49 -7
  20. package/ui/dashboard.js +379 -93
  21. package/ui/docs/docs.css +29 -0
  22. package/ui/docs/index.html +416 -119
  23. package/ui/docs/screenshots/asset-type-cards.png +0 -0
  24. package/ui/docs/screenshots/brand-kit-preview.png +0 -0
  25. package/ui/docs/screenshots/content-type-dropdown.png +0 -0
  26. package/ui/docs/screenshots/deploy-progress.png +0 -0
  27. package/ui/docs/screenshots/editor-full-layout.png +0 -0
  28. package/ui/docs/screenshots/email-client-preview.png +0 -0
  29. package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
  30. package/ui/docs/screenshots/module-overview-slideout.png +0 -0
  31. package/ui/docs/screenshots/multi-page-tree.png +0 -0
  32. package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
  33. package/ui/docs/screenshots/pipeline-progress.png +0 -0
  34. package/ui/docs/screenshots/project-overview-table.png +0 -0
  35. package/ui/docs/screenshots/split-pane-view.png +0 -0
  36. package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
  37. package/ui/docs/screenshots/workspace-tabs.png +0 -0
  38. package/ui/email-preview.js +109 -0
  39. package/ui/field-editor.js +72 -1
  40. package/ui/icons.js +120 -0
  41. package/ui/index.html +877 -629
  42. package/ui/inline-edit.js +710 -0
  43. package/ui/plan.js +0 -0
  44. package/ui/preview.js +101 -198
  45. package/ui/section-controls.js +628 -0
  46. package/ui/settings.js +58 -16
  47. package/ui/setup.js +750 -140
  48. package/ui/styles.css +3430 -952
  49. package/ui/upload-panel.js +47 -20
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Email client preview — heuristic rendering for Gmail, Outlook Desktop, Apple Mail.
3
+ * Only visible in email content mode.
4
+ */
5
+
6
+ (function () {
7
+ const overlay = document.getElementById("email-preview-overlay");
8
+ const closeBtn = document.getElementById("email-preview-close");
9
+ const openBtn = document.getElementById("btn-email-preview");
10
+ const tabsContainer = document.getElementById("email-preview-tabs");
11
+ const frame = document.getElementById("email-preview-frame");
12
+ const notesEl = document.getElementById("email-preview-notes");
13
+
14
+ if (!overlay || !frame) return;
15
+
16
+ let cachedPreviews = null;
17
+ let activeClient = "gmail";
18
+
19
+ function showPreview(client) {
20
+ activeClient = client;
21
+ tabsContainer.querySelectorAll(".email-preview-tab").forEach(function (btn) {
22
+ btn.classList.toggle("active", btn.getAttribute("data-client") === client);
23
+ });
24
+
25
+ if (!cachedPreviews) return;
26
+ var preview = cachedPreviews.find(function (p) { return p.client === client; });
27
+ if (!preview) return;
28
+
29
+ frame.srcdoc = preview.html;
30
+
31
+ if (preview.notes && preview.notes.length > 0) {
32
+ notesEl.innerHTML = preview.notes
33
+ .map(function (n) { return '<span class="email-preview-note">' + escapeHtml(n) + '</span>'; })
34
+ .join("");
35
+ notesEl.classList.remove("hidden");
36
+ } else {
37
+ notesEl.classList.add("hidden");
38
+ }
39
+ }
40
+
41
+ function escapeHtml(str) {
42
+ var div = document.createElement("div");
43
+ div.textContent = str;
44
+ return div.innerHTML;
45
+ }
46
+
47
+ function openEmailPreview() {
48
+ overlay.classList.remove("hidden");
49
+ notesEl.textContent = "Loading previews...";
50
+ notesEl.classList.remove("hidden");
51
+
52
+ fetch("/api/email-preview/render", {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({}),
56
+ })
57
+ .then(function (res) { return res.json(); })
58
+ .then(function (data) {
59
+ if (data.error) {
60
+ notesEl.textContent = "Error: " + data.error;
61
+ return;
62
+ }
63
+ cachedPreviews = data.previews;
64
+ showPreview(activeClient);
65
+ })
66
+ .catch(function (err) {
67
+ notesEl.textContent = "Failed to load previews: " + err.message;
68
+ });
69
+ }
70
+
71
+ function closeEmailPreview() {
72
+ overlay.classList.add("hidden");
73
+ cachedPreviews = null;
74
+ frame.srcdoc = "";
75
+ }
76
+
77
+ if (openBtn) {
78
+ openBtn.addEventListener("click", openEmailPreview);
79
+ }
80
+
81
+ if (closeBtn) {
82
+ closeBtn.addEventListener("click", closeEmailPreview);
83
+ }
84
+
85
+ overlay.addEventListener("click", function (e) {
86
+ if (e.target === overlay) closeEmailPreview();
87
+ });
88
+
89
+ tabsContainer.addEventListener("click", function (e) {
90
+ var btn = e.target.closest(".email-preview-tab");
91
+ if (!btn) return;
92
+ var client = btn.getAttribute("data-client");
93
+ if (client) showPreview(client);
94
+ });
95
+
96
+ document.addEventListener("keydown", function (e) {
97
+ if (e.key === "Escape" && !overlay.classList.contains("hidden")) {
98
+ closeEmailPreview();
99
+ }
100
+ });
101
+
102
+ window.showEmailPreviewButton = function () {
103
+ if (openBtn) openBtn.classList.remove("hidden");
104
+ };
105
+
106
+ window.hideEmailPreviewButton = function () {
107
+ if (openBtn) openBtn.classList.add("hidden");
108
+ };
109
+ })();
@@ -14,6 +14,7 @@ let currentEditModule = null;
14
14
  async function openFieldEditor(moduleName) {
15
15
  currentEditModule = moduleName;
16
16
  editorTitle.textContent = moduleName;
17
+ switchFieldEditorTab("fields");
17
18
 
18
19
  // Switch slideout to editor view
19
20
  if (typeof showEditorView === "function") {
@@ -26,7 +27,7 @@ async function openFieldEditor(moduleName) {
26
27
  const data = await res.json();
27
28
  const mod = data.modules.find((m) => m.moduleName === moduleName);
28
29
  if (!mod) {
29
- editorContent.innerHTML = "<p>Section not found</p>";
30
+ editorContent.innerHTML = "<p>Module not found</p>";
30
31
  return;
31
32
  }
32
33
 
@@ -293,3 +294,73 @@ function updateField(moduleName, fieldPath, value) {
293
294
  }).then(() => refreshPreview());
294
295
  }, 300);
295
296
  }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Field editor tabs (Fields / Code)
300
+ // ---------------------------------------------------------------------------
301
+
302
+ const fieldEditorCode = document.getElementById("field-editor-code");
303
+
304
+ function switchFieldEditorTab(tab) {
305
+ const tabFields = document.getElementById("field-editor-tab-fields");
306
+ const tabCode = document.getElementById("field-editor-tab-code");
307
+
308
+ if (tab === "code") {
309
+ editorContent.classList.add("hidden");
310
+ if (fieldEditorCode) fieldEditorCode.classList.remove("hidden");
311
+ if (tabFields) tabFields.classList.remove("active");
312
+ if (tabCode) tabCode.classList.add("active");
313
+ if (currentEditModule) showModuleCode(currentEditModule);
314
+ } else {
315
+ editorContent.classList.remove("hidden");
316
+ if (fieldEditorCode) fieldEditorCode.classList.add("hidden");
317
+ if (tabFields) tabFields.classList.add("active");
318
+ if (tabCode) tabCode.classList.remove("active");
319
+ }
320
+ }
321
+
322
+ document.getElementById("field-editor-tab-fields")?.addEventListener("click", () => switchFieldEditorTab("fields"));
323
+ document.getElementById("field-editor-tab-code")?.addEventListener("click", () => switchFieldEditorTab("code"));
324
+
325
+ function escHtml(s) {
326
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
327
+ }
328
+
329
+ function prettyJsonSafe(str) {
330
+ try { return JSON.stringify(JSON.parse(str), null, 2); } catch { return str; }
331
+ }
332
+
333
+ async function showModuleCode(moduleName) {
334
+ if (!fieldEditorCode) return;
335
+ fieldEditorCode.innerHTML = "<p style='padding:12px;color:var(--text-dim)'>Loading…</p>";
336
+
337
+ try {
338
+ const res = await fetch("/api/modules");
339
+ const data = await res.json();
340
+ const mod = data.modules.find((m) => m.moduleName === moduleName);
341
+ if (!mod) {
342
+ fieldEditorCode.innerHTML = "<p style='padding:12px'>Module not found</p>";
343
+ return;
344
+ }
345
+
346
+ const sections = [
347
+ { label: "module.html", code: mod.html || "" },
348
+ { label: "module.css", code: mod.css || "" },
349
+ { label: "module.js", code: mod.js || "" },
350
+ { label: "fields.json", code: prettyJsonSafe(mod.fieldsJson || "[]") },
351
+ ];
352
+
353
+ fieldEditorCode.innerHTML = sections
354
+ .filter((s) => s.code.trim())
355
+ .map(
356
+ (s) => `
357
+ <details class="field-editor__code-file" open>
358
+ <summary class="field-editor__code-file-header">${escHtml(s.label)}</summary>
359
+ <pre class="field-editor__code-pre"><code>${escHtml(s.code)}</code></pre>
360
+ </details>`
361
+ )
362
+ .join("");
363
+ } catch (err) {
364
+ fieldEditorCode.innerHTML = `<p style="padding:12px">Error: ${escHtml(err.message)}</p>`;
365
+ }
366
+ }
package/ui/icons.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * SVG icon system — Material Design-inspired, stroke-based icons.
3
+ * Follows existing codebase convention: 24×24 viewBox, stroke="currentColor",
4
+ * stroke-width from --icon-stroke (1.5), round line caps/joins.
5
+ */
6
+
7
+ /* eslint-disable max-len */
8
+ const VS_ICON_PATHS = {
9
+ // Sparkle / auto-awesome — 4-pointed star burst
10
+ sparkle: '<path d="M12 2L14.09 8.26L20 9.27L15.55 13.97L16.91 20L12 16.9L7.09 20L8.45 13.97L4 9.27L9.91 8.26L12 2Z"/>',
11
+
12
+ // Settings / gear
13
+ settings: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
14
+
15
+ // Check — simple checkmark
16
+ check: '<polyline points="20 6 9 17 4 12"/>',
17
+
18
+ // Check circle — checkmark inside circle
19
+ "check-circle": '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
20
+
21
+ // X circle — error/failure
22
+ "x-circle": '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>',
23
+
24
+ // Rocket — celebration / deploy success
25
+ rocket: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>',
26
+
27
+ // Copy — clone/duplicate
28
+ copy: '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
29
+
30
+ // Star — filled star
31
+ star: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
32
+
33
+ // Chevron down
34
+ "chevron-down": '<polyline points="6 9 12 15 18 9"/>',
35
+
36
+ // Send / arrow-up (for submit button)
37
+ send: '<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>',
38
+
39
+ // Arrow up
40
+ "arrow-up": '<line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/>',
41
+
42
+ // Plus
43
+ plus: '<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>',
44
+
45
+ // Download
46
+ download: '<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"/>',
47
+
48
+ // Diamond — design/Figma
49
+ diamond: '<path d="M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0z"/>',
50
+
51
+ // Refresh / convert
52
+ "refresh-cw": '<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>',
53
+
54
+ // Auto-awesome / sparkle stars (for suggestion chips)
55
+ "auto-awesome": '<path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z"/><path d="M20 5l.8 2.4L23 8.2l-2.2.8L20 11.4l-.8-2.4-2.2-.8 2.2-.8z" opacity=".6"/>',
56
+
57
+ // Wave / hand — greeting
58
+ hand: '<path d="M18 11V6a1 1 0 0 0-2 0"/><path d="M16 8V4a1 1 0 0 0-2 0v1"/><path d="M14 8V5a1 1 0 0 0-2 0v5"/><path d="M12 13V8.5a1 1 0 0 0-2 0V14"/><path d="M20 15.5c0 3.59-2.91 6.5-6.5 6.5H12c-3.31 0-6-2.69-6-6V9a1 1 0 0 1 2 0v4"/><path d="M18 11a1 1 0 0 1 2 0v3.5"/>',
59
+
60
+ // File / document — landing page
61
+ file: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>',
62
+
63
+ // Mail / envelope — email
64
+ mail: '<rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="22 4 12 13 2 4"/>',
65
+
66
+ // Globe — website
67
+ globe: '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>',
68
+
69
+ // Edit / pencil — blog post
70
+ edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
71
+
72
+ // Palette — template
73
+ palette: '<circle cx="13.5" cy="6.5" r="1.5"/><circle cx="17.5" cy="10.5" r="1.5"/><circle cx="8.5" cy="7.5" r="1.5"/><circle cx="6.5" cy="12" r="1.5"/><path d="M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10c.55 0 1-.45 1-1v-1.5c0-.83.67-1.5 1.5-1.5H16a4 4 0 0 0 4-4c0-4.42-3.58-8-8-8z"/>',
74
+
75
+ // Inbox / import — download into
76
+ inbox: '<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
77
+
78
+ // Arrow left — back button
79
+ "arrow-left": '<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>'
80
+ };
81
+ /* eslint-enable max-len */
82
+
83
+ /**
84
+ * Return an inline SVG string for the named icon.
85
+ * @param {string} name — key from VS_ICON_PATHS
86
+ * @param {object} [opts]
87
+ * @param {string} [opts.size] — CSS class: "sm" (16 px) | "md" (20 px, default) | custom px value
88
+ * @param {string} [opts.class] — extra CSS classes
89
+ * @param {string} [opts.ariaLabel] — accessible label (omit for decorative icons)
90
+ * @returns {string} SVG markup
91
+ */
92
+ function vsIcon(name, opts) {
93
+ const o = opts || {};
94
+ const paths = VS_ICON_PATHS[name];
95
+ if (!paths) return "";
96
+
97
+ const sizeClass = o.size === "sm" ? " icon--sm" : o.size === "md" ? " icon--md" : "";
98
+ const extra = o.class ? " " + o.class : "";
99
+ const cls = "vs-icon" + sizeClass + extra;
100
+
101
+ const ariaAttrs = o.ariaLabel
102
+ ? `role="img" aria-label="${o.ariaLabel}"`
103
+ : 'aria-hidden="true"';
104
+
105
+ return `<svg class="${cls}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" ${ariaAttrs}>${paths}</svg>`;
106
+ }
107
+
108
+ /**
109
+ * Return a CSS-safe SVG data URI for use in content / background-image.
110
+ * @param {string} name — key from VS_ICON_PATHS
111
+ * @param {string} [color] — stroke color (default: currentColor placeholder, use %23 for #)
112
+ * @returns {string} url("data:image/svg+xml,...")
113
+ */
114
+ function vsIconDataUri(name, color) {
115
+ const paths = VS_ICON_PATHS[name];
116
+ if (!paths) return "none";
117
+ const c = color || "currentColor";
118
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='${c}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'>${paths}</svg>`;
119
+ return "url(\"data:image/svg+xml," + encodeURIComponent(svg) + "\")";
120
+ }