vibespot 1.0.6 → 1.0.8
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/dist/index.js +61 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/ui/docs/docs.css +977 -0
- package/ui/docs/docs.js +268 -0
- package/ui/docs/index.html +1242 -0
- package/ui/index.html +8 -0
- package/ui/styles.css +1 -0
package/ui/docs/docs.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/* vibeSpot Documentation — Search, Navigation, Interactive Elements */
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
// ================================================================
|
|
7
|
+
// Sidebar scroll spy — highlight active section on scroll
|
|
8
|
+
// ================================================================
|
|
9
|
+
const navLinks = document.querySelectorAll(".doc-nav__link[href^='#']");
|
|
10
|
+
const sections = [];
|
|
11
|
+
|
|
12
|
+
navLinks.forEach((link) => {
|
|
13
|
+
const id = link.getAttribute("href").slice(1);
|
|
14
|
+
const el = document.getElementById(id);
|
|
15
|
+
if (el) sections.push({ id, el, link });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let ticking = false;
|
|
19
|
+
function onScroll() {
|
|
20
|
+
if (ticking) return;
|
|
21
|
+
ticking = true;
|
|
22
|
+
requestAnimationFrame(() => {
|
|
23
|
+
const scrollY = window.scrollY + 100;
|
|
24
|
+
let current = sections[0];
|
|
25
|
+
for (const s of sections) {
|
|
26
|
+
if (s.el.offsetTop <= scrollY) current = s;
|
|
27
|
+
}
|
|
28
|
+
navLinks.forEach((l) => l.classList.remove("active"));
|
|
29
|
+
if (current) current.link.classList.add("active");
|
|
30
|
+
ticking = false;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
34
|
+
onScroll();
|
|
35
|
+
|
|
36
|
+
// ================================================================
|
|
37
|
+
// Collapsible sections
|
|
38
|
+
// ================================================================
|
|
39
|
+
document.querySelectorAll(".doc-collapse__trigger").forEach((btn) => {
|
|
40
|
+
btn.addEventListener("click", () => {
|
|
41
|
+
btn.closest(".doc-collapse").classList.toggle("open");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ================================================================
|
|
46
|
+
// Tabs
|
|
47
|
+
// ================================================================
|
|
48
|
+
document.querySelectorAll(".doc-tabs").forEach((tabGroup) => {
|
|
49
|
+
const tabs = tabGroup.querySelectorAll(".doc-tabs__tab");
|
|
50
|
+
const panels = tabGroup.querySelectorAll(".doc-tabs__panel");
|
|
51
|
+
tabs.forEach((tab) => {
|
|
52
|
+
tab.addEventListener("click", () => {
|
|
53
|
+
tabs.forEach((t) => t.classList.remove("active"));
|
|
54
|
+
panels.forEach((p) => p.classList.remove("active"));
|
|
55
|
+
tab.classList.add("active");
|
|
56
|
+
const target = tabGroup.querySelector(`#${tab.dataset.tab}`);
|
|
57
|
+
if (target) target.classList.add("active");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ================================================================
|
|
63
|
+
// Code copy buttons
|
|
64
|
+
// ================================================================
|
|
65
|
+
document.querySelectorAll("pre").forEach((pre) => {
|
|
66
|
+
const btn = document.createElement("button");
|
|
67
|
+
btn.className = "doc-code-copy";
|
|
68
|
+
btn.textContent = "Copy";
|
|
69
|
+
btn.addEventListener("click", () => {
|
|
70
|
+
const code = pre.querySelector("code")?.textContent || pre.textContent;
|
|
71
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
72
|
+
btn.textContent = "Copied!";
|
|
73
|
+
setTimeout(() => { btn.textContent = "Copy"; }, 1500);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
pre.appendChild(btn);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ================================================================
|
|
80
|
+
// Search
|
|
81
|
+
// ================================================================
|
|
82
|
+
const searchInput = document.getElementById("doc-search-input");
|
|
83
|
+
const searchResults = document.getElementById("doc-search-results");
|
|
84
|
+
if (!searchInput || !searchResults) return;
|
|
85
|
+
|
|
86
|
+
// Build search index from all headings and paragraphs
|
|
87
|
+
const searchIndex = [];
|
|
88
|
+
document.querySelectorAll(".doc-section").forEach((section) => {
|
|
89
|
+
const sectionTitle = section.querySelector("h2")?.textContent || "";
|
|
90
|
+
section.querySelectorAll("h2, h3, h4, p, li, td, .doc-callout").forEach((el) => {
|
|
91
|
+
const text = el.textContent.trim();
|
|
92
|
+
if (!text || text.length < 10) return;
|
|
93
|
+
const heading = el.closest("h2, h3, h4") ? el : el.previousElementSibling;
|
|
94
|
+
const title = el.tagName.match(/^H[234]$/) ? text : (
|
|
95
|
+
findNearestHeading(el)?.textContent || sectionTitle
|
|
96
|
+
);
|
|
97
|
+
const id = findNearestId(el) || section.id;
|
|
98
|
+
searchIndex.push({ title, text: text.slice(0, 300), section: sectionTitle, id });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function findNearestHeading(el) {
|
|
103
|
+
let node = el.previousElementSibling;
|
|
104
|
+
while (node) {
|
|
105
|
+
if (node.tagName && node.tagName.match(/^H[234]$/)) return node;
|
|
106
|
+
node = node.previousElementSibling;
|
|
107
|
+
}
|
|
108
|
+
return el.closest(".doc-section")?.querySelector("h2");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function findNearestId(el) {
|
|
112
|
+
let node = el;
|
|
113
|
+
while (node) {
|
|
114
|
+
if (node.id) return node.id;
|
|
115
|
+
const prev = node.previousElementSibling;
|
|
116
|
+
if (prev?.id) return prev.id;
|
|
117
|
+
node = node.parentElement;
|
|
118
|
+
}
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let focusedIdx = -1;
|
|
123
|
+
|
|
124
|
+
searchInput.addEventListener("input", () => {
|
|
125
|
+
const q = searchInput.value.trim().toLowerCase();
|
|
126
|
+
if (q.length < 2) {
|
|
127
|
+
searchResults.classList.remove("visible");
|
|
128
|
+
searchResults.innerHTML = "";
|
|
129
|
+
focusedIdx = -1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const words = q.split(/\s+/);
|
|
134
|
+
const matches = [];
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
|
|
137
|
+
for (const entry of searchIndex) {
|
|
138
|
+
const lower = (entry.title + " " + entry.text).toLowerCase();
|
|
139
|
+
if (words.every((w) => lower.includes(w))) {
|
|
140
|
+
const key = entry.id + "|" + entry.title.slice(0, 40);
|
|
141
|
+
if (seen.has(key)) continue;
|
|
142
|
+
seen.add(key);
|
|
143
|
+
matches.push(entry);
|
|
144
|
+
if (matches.length >= 12) break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (matches.length === 0) {
|
|
149
|
+
searchResults.innerHTML = '<div class="doc-search-empty">No results found</div>';
|
|
150
|
+
searchResults.classList.add("visible");
|
|
151
|
+
focusedIdx = -1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
searchResults.innerHTML = matches.map((m, i) => {
|
|
156
|
+
const snippet = highlightSnippet(m.text, words);
|
|
157
|
+
return `<a class="doc-search-result" href="#${m.id}" data-idx="${i}">
|
|
158
|
+
<div class="doc-search-result__title">${escapeHtml(m.title)}<span class="doc-search-result__section">${escapeHtml(m.section)}</span></div>
|
|
159
|
+
<div class="doc-search-result__snippet">${snippet}</div>
|
|
160
|
+
</a>`;
|
|
161
|
+
}).join("");
|
|
162
|
+
searchResults.classList.add("visible");
|
|
163
|
+
focusedIdx = -1;
|
|
164
|
+
|
|
165
|
+
// Click result → navigate and close
|
|
166
|
+
searchResults.querySelectorAll(".doc-search-result").forEach((r) => {
|
|
167
|
+
r.addEventListener("click", () => {
|
|
168
|
+
searchResults.classList.remove("visible");
|
|
169
|
+
searchInput.value = "";
|
|
170
|
+
closeSidebar();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
searchInput.addEventListener("keydown", (e) => {
|
|
176
|
+
const items = searchResults.querySelectorAll(".doc-search-result");
|
|
177
|
+
if (!items.length) return;
|
|
178
|
+
|
|
179
|
+
if (e.key === "ArrowDown") {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
focusedIdx = Math.min(focusedIdx + 1, items.length - 1);
|
|
182
|
+
updateFocus(items);
|
|
183
|
+
} else if (e.key === "ArrowUp") {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
focusedIdx = Math.max(focusedIdx - 1, 0);
|
|
186
|
+
updateFocus(items);
|
|
187
|
+
} else if (e.key === "Enter" && focusedIdx >= 0) {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
items[focusedIdx].click();
|
|
190
|
+
} else if (e.key === "Escape") {
|
|
191
|
+
searchResults.classList.remove("visible");
|
|
192
|
+
searchInput.blur();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
function updateFocus(items) {
|
|
197
|
+
items.forEach((el, i) => el.classList.toggle("focused", i === focusedIdx));
|
|
198
|
+
if (focusedIdx >= 0) items[focusedIdx].scrollIntoView({ block: "nearest" });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Close search on outside click
|
|
202
|
+
document.addEventListener("click", (e) => {
|
|
203
|
+
if (!e.target.closest(".doc-topbar__search") && !e.target.closest(".doc-search-results")) {
|
|
204
|
+
searchResults.classList.remove("visible");
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Keyboard shortcut: / to focus search
|
|
209
|
+
document.addEventListener("keydown", (e) => {
|
|
210
|
+
if (e.key === "/" && !e.target.closest("input, textarea, [contenteditable]")) {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
searchInput.focus();
|
|
213
|
+
}
|
|
214
|
+
if (e.key === "Escape") {
|
|
215
|
+
searchResults.classList.remove("visible");
|
|
216
|
+
searchInput.blur();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
function highlightSnippet(text, words) {
|
|
221
|
+
let s = escapeHtml(text.slice(0, 160));
|
|
222
|
+
for (const w of words) {
|
|
223
|
+
const re = new RegExp(`(${w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
|
|
224
|
+
s = s.replace(re, "<mark>$1</mark>");
|
|
225
|
+
}
|
|
226
|
+
return s + (text.length > 160 ? "..." : "");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function escapeHtml(s) {
|
|
230
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ================================================================
|
|
234
|
+
// Mobile sidebar toggle
|
|
235
|
+
// ================================================================
|
|
236
|
+
const menuBtn = document.getElementById("doc-menu-btn");
|
|
237
|
+
const sidebar = document.querySelector(".doc-sidebar");
|
|
238
|
+
|
|
239
|
+
if (menuBtn && sidebar) {
|
|
240
|
+
menuBtn.addEventListener("click", () => {
|
|
241
|
+
sidebar.classList.toggle("open");
|
|
242
|
+
});
|
|
243
|
+
// Close sidebar on nav link click (mobile)
|
|
244
|
+
sidebar.querySelectorAll(".doc-nav__link").forEach((l) => {
|
|
245
|
+
l.addEventListener("click", () => closeSidebar());
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function closeSidebar() {
|
|
250
|
+
if (sidebar) sidebar.classList.remove("open");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ================================================================
|
|
254
|
+
// Smooth scroll for sidebar links (close sidebar on mobile)
|
|
255
|
+
// ================================================================
|
|
256
|
+
navLinks.forEach((link) => {
|
|
257
|
+
link.addEventListener("click", (e) => {
|
|
258
|
+
const id = link.getAttribute("href").slice(1);
|
|
259
|
+
const target = document.getElementById(id);
|
|
260
|
+
if (target) {
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
target.scrollIntoView({ behavior: "smooth" });
|
|
263
|
+
history.replaceState(null, "", "#" + id);
|
|
264
|
+
}
|
|
265
|
+
closeSidebar();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
})();
|