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.
- package/README.md +54 -5
- package/assets/blog-rules.md +251 -0
- package/assets/email-rules.md +390 -0
- package/assets/humanify-guide.md +300 -101
- package/assets/plan-templates/blog-content-hub.md +18 -9
- package/assets/plan-templates/email-announcement.md +41 -0
- package/assets/plan-templates/email-event-invite.md +43 -0
- package/assets/plan-templates/email-newsletter.md +41 -0
- package/assets/plan-templates/email-re-engagement.md +42 -0
- package/assets/plan-templates/email-welcome.md +41 -0
- package/dist/index.js +1460 -387
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/starters/06-blog-content-hub.json +75 -0
- package/starters/06-email-welcome.json +60 -0
- package/starters/07-email-announcement.json +60 -0
- package/starters/08-email-newsletter.json +52 -0
- package/ui/chat.js +777 -63
- package/ui/code-editor.js +49 -7
- package/ui/dashboard.js +379 -93
- package/ui/docs/docs.css +29 -0
- package/ui/docs/index.html +416 -119
- package/ui/docs/screenshots/asset-type-cards.png +0 -0
- package/ui/docs/screenshots/brand-kit-preview.png +0 -0
- package/ui/docs/screenshots/content-type-dropdown.png +0 -0
- package/ui/docs/screenshots/deploy-progress.png +0 -0
- package/ui/docs/screenshots/editor-full-layout.png +0 -0
- package/ui/docs/screenshots/email-client-preview.png +0 -0
- package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
- package/ui/docs/screenshots/module-overview-slideout.png +0 -0
- package/ui/docs/screenshots/multi-page-tree.png +0 -0
- package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
- package/ui/docs/screenshots/pipeline-progress.png +0 -0
- package/ui/docs/screenshots/project-overview-table.png +0 -0
- package/ui/docs/screenshots/split-pane-view.png +0 -0
- package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
- package/ui/docs/screenshots/workspace-tabs.png +0 -0
- package/ui/email-preview.js +109 -0
- package/ui/field-editor.js +72 -1
- package/ui/icons.js +120 -0
- package/ui/index.html +877 -629
- package/ui/inline-edit.js +710 -0
- package/ui/plan.js +0 -0
- package/ui/preview.js +101 -198
- package/ui/section-controls.js +628 -0
- package/ui/settings.js +58 -16
- package/ui/setup.js +750 -140
- package/ui/styles.css +3430 -952
- package/ui/upload-panel.js +47 -20
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
})();
|
package/ui/field-editor.js
CHANGED
|
@@ -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>
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
+
}
|