var THEME_KEY = "vyla-theme";
var ICONS = {
system: ' ',
light: ' ',
dark: ' ',
search: ' ',
menu: ' ',
doc: ' ',
hash: ' ',
copy: ' ',
check: ' ',
chevron: ' '
};
var searchData = null;
var searchResults = [];
var searchActive = 0;
function icon(name) {
return '' + ICONS[name] + " ";
}
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
function getThemeMode() {
var v = localStorage.getItem(THEME_KEY);
return v === "light" || v === "dark" ? v : "system";
}
function applyTheme() {
var mode = getThemeMode();
var dark = mode === "dark" || (mode === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", dark);
var logo = document.getElementById("brand-logo");
if (logo) logo.src = dark ? "/assets/images/logo.png" : "/assets/images/dark_logo.png";
}
function setTheme(mode) {
if (mode === "system") localStorage.removeItem(THEME_KEY);
else localStorage.setItem(THEME_KEY, mode);
applyTheme();
refreshThemeUI();
var menu = document.getElementById("theme-menu");
if (menu) menu.classList.remove("open");
}
function toggleThemeMenu(e) {
e.stopPropagation();
document.getElementById("theme-menu").classList.toggle("open");
}
function refreshThemeUI() {
var mode = getThemeMode();
var btn = document.getElementById("theme-toggle");
if (btn) btn.innerHTML = icon(mode);
document.querySelectorAll("#theme-menu button, #theme-seg button").forEach(function (b) {
b.classList.toggle("on", b.getAttribute("data-mode") === mode);
});
}
function buildThemeControls() {
var btn = document.getElementById("theme-toggle");
var links = document.querySelector("#navbar .links");
if (!btn || !links) return;
var wrap = document.createElement("div");
wrap.className = "theme-wrap";
btn.parentNode.insertBefore(wrap, btn);
wrap.appendChild(btn);
var modes = ["system", "light", "dark"];
var menu = document.createElement("div");
menu.id = "theme-menu";
menu.innerHTML = modes.map(function (m) {
return '' + icon(m) + "" + m.charAt(0).toUpperCase() + m.slice(1) + ' ✓ ';
}).join("");
wrap.appendChild(menu);
var seg = document.createElement("div");
seg.id = "theme-seg";
seg.innerHTML = modes.map(function (m) {
return '' + icon(m) + " ";
}).join("");
links.appendChild(seg);
[menu, seg].forEach(function (el) {
el.addEventListener("click", function (e) {
var b = e.target.closest("button");
if (b) setTheme(b.getAttribute("data-mode"));
});
});
refreshThemeUI();
}
function toggleSidebar() {
var sidebar = document.getElementById("sidebar");
var nav = document.getElementById("navbar");
var top = Math.max(0, Math.round(nav.getBoundingClientRect().bottom));
sidebar.style.setProperty("--sidebar-top", top + "px");
var open = sidebar.classList.toggle("open");
document.body.classList.toggle("nav-open", open);
}
function findActiveTab(root) {
var hrefs = {};
root.querySelectorAll("#sidebar .group li a").forEach(function (a) {
hrefs[a.getAttribute("href")] = true;
});
var tabs = root.querySelectorAll("#navbar .tabs a");
for (var i = 0; i < tabs.length; i++) {
if (hrefs[tabs[i].getAttribute("href")]) return tabs[i];
}
return root.querySelector("#navbar .tabs a.active");
}
function fixActiveTab() {
var current = findActiveTab(document);
if (!current) return;
document.querySelectorAll("#navbar .tabs a").forEach(function (a) {
a.classList.toggle("active", a === current);
});
}
function activeLink() {
return document.querySelector("#sidebar .group:not(.mobile-tabs) li a.active");
}
function activeGroupTitle() {
var a = activeLink();
var g = a && a.closest(".group");
var t = g && g.querySelector(".group-title");
return t ? t.textContent.trim() : "";
}
function buildMobileBar() {
var nav = document.getElementById("navbar");
var sidebar = document.getElementById("sidebar");
if (!nav) return;
var a = activeLink();
var group = activeGroupTitle();
var bar = document.createElement("div");
bar.id = "mobile-bar";
bar.innerHTML =
'' + icon("menu") + " " +
(group ? "" + esc(group) + ' › ' : "") +
'' + esc(a ? a.textContent.trim() : document.title) + " ";
nav.parentNode.insertBefore(bar, nav.nextSibling);
var tabs = document.querySelectorAll("#navbar .tabs a");
if (sidebar && tabs.length) {
var currentTab = document.querySelector("#navbar .tabs a.active") || tabs[0];
var g = document.createElement("div");
g.className = "mobile-tabs";
g.innerHTML =
'' + esc(currentTab.textContent.trim()) + " " + icon("chevron") + " " +
'
";
sidebar.insertBefore(g, sidebar.firstChild);
g.querySelector(".mt-btn").addEventListener("click", function () {
g.classList.toggle("open");
});
document.addEventListener("click", function (e) {
if (!g.contains(e.target)) g.classList.remove("open");
});
g.querySelectorAll(".mt-menu a").forEach(function (a) {
a.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
var href = a.getAttribute("href");
g.querySelectorAll(".mt-menu a").forEach(function (link) {
link.classList.remove("on");
});
a.classList.add("on");
g.querySelector(".mt-btn span").textContent = a.textContent.trim();
g.classList.remove("open");
var sidebar = document.getElementById("sidebar");
if (sidebar) {
var mobileTabs = sidebar.querySelector(".mobile-tabs");
var nav = document.getElementById("navbar");
var top = Math.max(0, Math.round(nav.getBoundingClientRect().bottom));
sidebar.style.setProperty("--sidebar-top", top + "px");
fetch(href)
.then(function (r) { return r.text(); })
.then(function (html) {
var doc = new DOMParser().parseFromString(html, "text/html");
var newSidebar = doc.querySelector("#sidebar");
if (newSidebar) {
var groups = newSidebar.querySelectorAll(".group");
sidebar.innerHTML = "";
if (mobileTabs) sidebar.appendChild(mobileTabs);
groups.forEach(function (group) {
sidebar.appendChild(group.cloneNode(true));
});
addEyebrow();
}
})
.catch(function () { });
if (!sidebar.classList.contains("open")) {
sidebar.classList.add("open");
document.body.classList.add("nav-open");
}
}
});
});
}
}
function addEyebrow() {
var h1 = document.querySelector("main.prose h1");
var group = activeGroupTitle();
if (!h1 || !group) return;
var el = document.createElement("div");
el.className = "eyebrow";
el.textContent = group;
h1.parentNode.insertBefore(el, h1);
}
function addCopyButtons() {
document.querySelectorAll(".code-group").forEach(function (cg) {
if (cg.querySelector(".cg-copy-btn")) return;
var b = document.createElement("button");
b.type = "button";
b.className = "cg-copy-btn";
b.setAttribute("aria-label", "Copy");
b.innerHTML = icon("copy");
b.onclick = function () {
var activePre = cg.querySelector(".code-group-panel.active pre");
if (!activePre) return;
navigator.clipboard.writeText(activePre.innerText).then(function () {
b.innerHTML = icon("check");
setTimeout(function () { b.innerHTML = icon("copy"); }, 1500);
});
};
cg.appendChild(b);
});
document.querySelectorAll("main.prose pre").forEach(function (pre) {
if (pre.closest(".code-group")) return;
if (pre.parentNode.classList.contains("code-wrap")) return;
var wrap = document.createElement("div");
wrap.className = "code-wrap";
pre.parentNode.insertBefore(wrap, pre);
wrap.appendChild(pre);
var b = document.createElement("button");
b.type = "button";
b.className = "copy-btn";
b.setAttribute("aria-label", "Copy");
b.innerHTML = icon("copy");
b.onclick = function () {
navigator.clipboard.writeText(pre.innerText).then(function () {
b.innerHTML = icon("check");
setTimeout(function () { b.innerHTML = icon("copy"); }, 1500);
});
};
wrap.appendChild(b);
});
}
function bindScrollFade(box, getScroller) {
function update() {
var el = getScroller();
var max = el.scrollWidth - el.clientWidth;
var left = 0;
var right = 0;
if (max > 1) {
left = Math.min(el.scrollLeft, 56);
right = Math.min(Math.max(Math.round(max - el.scrollLeft), 0), 56);
}
box.style.setProperty("--fade-l", left + "px");
box.style.setProperty("--fade-r", right + "px");
}
box.addEventListener("scroll", update, true);
window.addEventListener("resize", update);
window.addEventListener("load", update);
update();
}
function initCodeFades() {
document.querySelectorAll("main.prose pre").forEach(function (pre) {
var code = pre.querySelector("code");
bindScrollFade(pre, function () {
return code && code.scrollWidth > code.clientWidth + 1 ? code : pre;
});
});
}
function wrapTables() {
document.querySelectorAll("main.prose table").forEach(function (table) {
if (table.parentNode.classList.contains("table-wrap")) return;
var wrap = document.createElement("div");
wrap.className = "table-wrap";
table.parentNode.insertBefore(wrap, table);
wrap.appendChild(table);
bindScrollFade(wrap, function () {
return wrap;
});
});
}
function initTocSpy() {
var links = Array.prototype.slice.call(document.querySelectorAll("aside#toc a[href^='#']"));
if (!links.length) return;
var heads = links.map(function (a) {
return document.getElementById(decodeURIComponent(a.getAttribute("href").slice(1)));
});
function update() {
var idx = 0;
heads.forEach(function (h, i) {
if (h && h.getBoundingClientRect().top < 140) idx = i;
});
links.forEach(function (a, i) { a.classList.toggle("active", i === idx); });
}
window.addEventListener("scroll", update, { passive: true });
update();
}
var SEARCH_KEY = "vyla-search-v1";
var searchPromise = null;
function getRecent() {
try { return JSON.parse(localStorage.getItem("vyla-recent-v2")) || []; } catch (e) { return []; }
}
function saveRecent(item) {
if (!item) return;
var slim = { title: item.title, crumbs: item.crumbs, href: item.href, type: item.type, text: "" };
var list = getRecent().filter(function (r) { return r.href !== slim.href; });
list.unshift(slim);
localStorage.setItem("vyla-recent-v2", JSON.stringify(list.slice(0, 5)));
}
function cleanText(s) {
return s.replace(/\s+/g, " ").trim();
}
function fetchDoc(href) {
return fetch(href)
.then(function (r) {
if (!r.ok) throw new Error(href);
return r.text();
})
.then(function (html) {
return { href: href, doc: new DOMParser().parseFromString(html, "text/html") };
})
.catch(function () {
return null;
});
}
function indexDoc(res) {
var doc = res.doc;
var href = res.href.split("#")[0];
var prose = doc.querySelector("main.prose");
if (!prose) return [];
var tabEl = findActiveTab(doc);
var activeEl = doc.querySelector("#sidebar li a.active");
var groupEl = activeEl && activeEl.closest(".group");
var groupTitleEl = groupEl && groupEl.querySelector(".group-title");
var h1 = prose.querySelector("h1");
var pageTitle = h1 ? cleanText(h1.textContent) : activeEl ? cleanText(activeEl.textContent) : href;
var base = [];
if (tabEl) base.push(cleanText(tabEl.textContent));
if (groupTitleEl) base.push(cleanText(groupTitleEl.textContent));
var current = { title: pageTitle, id: "", type: "page", crumbs: base.slice(), text: [] };
var entries = [current];
Array.prototype.forEach.call(prose.children, function (el) {
if (el.tagName === "H1") return;
if (el.tagName === "H2" || el.tagName === "H3") {
var title = cleanText(el.textContent);
current = { title: title, id: el.id, type: "heading", crumbs: base.concat([pageTitle, title]), text: [] };
entries.push(current);
return;
}
current.text.push(cleanText(el.textContent));
});
return entries.map(function (e) {
return {
title: e.title,
crumbs: e.crumbs,
href: e.id ? href + "#" + e.id : href,
type: e.type,
text: e.text.join(" ").slice(0, 4000)
};
});
}
function crawlSite() {
var seen = {};
var all = [];
function visit(hrefs) {
var fresh = hrefs.map(function (h) {
return new URL(h, window.location.href);
}).filter(function (u) {
if (u.origin !== window.location.origin || seen[u.pathname]) return false;
seen[u.pathname] = true;
return true;
}).map(function (u) {
return u.pathname;
});
return Promise.all(fresh.map(fetchDoc)).then(function (results) {
var next = [];
results.forEach(function (res) {
if (!res) return;
all = all.concat(indexDoc(res));
res.doc.querySelectorAll("#sidebar li a").forEach(function (a) {
var h = a.getAttribute("href");
if (h && h.charAt(0) !== "#") next.push(h);
});
});
return next.length ? visit(next) : all;
});
}
var start = Array.prototype.map.call(document.querySelectorAll("#navbar .tabs a"), function (a) {
return a.getAttribute("href");
});
start.push(window.location.pathname);
return visit(start).then(function () {
return all;
});
}
function loadSearchData() {
if (searchData) return Promise.resolve(searchData);
if (searchPromise) return searchPromise;
try {
var cached = JSON.parse(sessionStorage.getItem(SEARCH_KEY));
if (cached && cached.data && cached.data.length && Date.now() - cached.time < 600000) {
searchData = cached.data;
return Promise.resolve(searchData);
}
} catch (e) { }
searchPromise = crawlSite().then(function (all) {
searchData = all;
try { sessionStorage.setItem(SEARCH_KEY, JSON.stringify({ time: Date.now(), data: all })); } catch (e) { }
return all;
});
return searchPromise;
}
function escRe(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function hl(text, tokens) {
if (!tokens.length) return esc(text);
var re = new RegExp("(" + tokens.map(escRe).join("|") + ")", "ig");
return text.split(re).map(function (part, i) {
return i % 2 ? '' + esc(part) + " " : esc(part);
}).join("");
}
function lower(it) {
if (!it._l) it._l = { t: it.title.toLowerCase(), c: it.crumbs.join(" ").toLowerCase(), x: (it.text || "").toLowerCase() };
return it._l;
}
function scoreEntry(it, tokens, q) {
var low = lower(it);
var score = 0;
for (var i = 0; i < tokens.length; i++) {
var t = tokens[i];
var inTitle = low.t.indexOf(t) !== -1;
var inCrumbs = low.c.indexOf(t) !== -1;
var inText = low.x.indexOf(t) !== -1;
if (!inTitle && !inCrumbs && !inText) return 0;
score += (inTitle ? 20 : 0) + (inCrumbs ? 4 : 0) + (inText ? 1 : 0);
}
if (low.t === q) score += 100;
else if (low.t.indexOf(q) === 0) score += 50;
else if (low.t.indexOf(q) !== -1) score += 25;
if (it.type === "page") score += 5;
return score;
}
function makeSnippet(it, tokens) {
var low = lower(it);
var inTitle = tokens.every(function (t) { return low.t.indexOf(t) !== -1; });
if (inTitle || !it.text) return "";
var hit = -1;
tokens.forEach(function (t) {
var k = low.x.indexOf(t);
if (k !== -1 && (hit === -1 || k < hit)) hit = k;
});
if (hit === -1) return "";
var start = Math.max(0, hit - 50);
var end = hit + 120;
return (start > 0 ? "…" : "") + it.text.slice(start, end) + (end < it.text.length ? "…" : "");
}
function renderSearch(keepActive) {
var q = document.getElementById("search-input").value.trim().toLowerCase();
var tokens = q.split(/\s+/).filter(Boolean);
var box = document.getElementById("search-results");
if (!keepActive) searchActive = 0;
if (tokens.length) {
if (!searchData) {
searchResults = [];
box.innerHTML = 'Indexing pages...
';
return;
}
searchResults = searchData
.map(function (it) { return { it: it, s: scoreEntry(it, tokens, q) }; })
.filter(function (r) { return r.s > 0; })
.sort(function (a, b) { return b.s - a.s; })
.slice(0, 12)
.map(function (r) { return r.it; });
} else {
searchResults = getRecent();
}
if (!searchResults.length) {
box.innerHTML = '' + (tokens.length ? "No results found" : "No recent searches") + "
";
return;
}
box.innerHTML = (tokens.length ? "" : 'Recent searches
') + searchResults.map(function (it, i) {
var snippet = tokens.length ? makeSnippet(it, tokens) : "";
var crumbs = it.crumbs.map(function (c) { return hl(c, tokens); }).join('› ');
return '' +
icon(it.type === "heading" ? "hash" : "doc") +
'' + esc(it.title) + "
" +
(snippet ? '
' + hl(snippet, tokens) + "
" : "") +
'
' + crumbs + "
";
}).join("");
if (keepActive) {
var on = box.querySelector(".sr.on");
if (on) on.scrollIntoView({ block: "nearest" });
}
}
function openSearch() {
var input = document.getElementById("search-input");
document.getElementById("search-modal").classList.add("open");
document.body.style.overflow = "hidden";
input.value = "";
renderSearch(false);
loadSearchData().then(function () { renderSearch(false); });
input.focus();
}
function closeSearch() {
document.getElementById("search-modal").classList.remove("open");
document.body.style.overflow = "";
}
function onSearchKey(e) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
openSearch();
return;
}
var modal = document.getElementById("search-modal");
if (!modal || !modal.classList.contains("open")) return;
if (e.key === "Escape") {
closeSearch();
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
var n = searchResults.length || 1;
searchActive = (searchActive + (e.key === "ArrowDown" ? 1 : -1) + n) % n;
renderSearch(true);
} else if (e.key === "Enter" && searchResults[searchActive]) {
var item = searchResults[searchActive];
saveRecent(item);
closeSearch();
window.location.href = item.href;
}
}
function buildSearch() {
var trigger = document.getElementById("search-trigger");
if (trigger) trigger.innerHTML = icon("search") + "Search... Ctrl K ";
var modal = document.createElement("div");
modal.id = "search-modal";
modal.innerHTML =
'
' + icon("search") +
'
' +
'
';
document.body.appendChild(modal);
modal.querySelector(".search-backdrop").addEventListener("click", closeSearch);
document.getElementById("search-input").addEventListener("input", function () { renderSearch(false); });
document.getElementById("search-results").addEventListener("click", function (e) {
var a = e.target.closest(".sr");
if (a) {
saveRecent(searchResults[Number(a.getAttribute("data-i"))]);
closeSearch();
}
});
document.addEventListener("keydown", onSearchKey);
}
applyTheme();
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyTheme);
function dismissBanner() {
var banner = document.getElementById("banner");
if (banner) banner.setAttribute("data-dismissed", "true");
document.body.classList.add("banner-dismissed");
localStorage.setItem("vyla-banner-dismissed", "true");
}
function cgSwitch(btn) {
var group = btn.closest(".code-group");
var index = btn.getAttribute("data-tab-index");
group.querySelectorAll(".code-group-tab").forEach(function (t) {
t.classList.toggle("active", t === btn);
});
group.querySelectorAll(".code-group-panel").forEach(function (p, i) {
p.classList.toggle("active", String(i) === index);
});
window.dispatchEvent(new Event("resize"));
}
document.addEventListener("DOMContentLoaded", function () {
if (localStorage.getItem("vyla-banner-dismissed") === "true") {
var banner = document.getElementById("banner");
if (banner) banner.setAttribute("data-dismissed", "true");
document.body.classList.add("banner-dismissed");
}
applyTheme();
fixActiveTab();
buildMobileBar();
addEyebrow();
addCopyButtons();
initCodeFades();
wrapTables();
buildThemeControls();
buildSearch();
setTimeout(loadSearchData, 1500);
initTocSpy();
document.addEventListener("click", function (e) {
var sidebar = document.getElementById("sidebar");
var menu = document.getElementById("theme-menu");
if (menu && !e.target.closest(".theme-wrap")) menu.classList.remove("open");
if (
sidebar &&
sidebar.classList.contains("open") &&
!sidebar.contains(e.target) &&
!e.target.closest("#mobile-toggle") &&
!e.target.closest(".theme-wrap") &&
!e.target.closest("#theme-seg")
) {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
}
});
var sidebarLinks = document.querySelectorAll("#sidebar .group:not(.mobile-tabs) li a");
sidebarLinks.forEach(function (a) {
a.addEventListener("click", function () {
var sidebar = document.getElementById("sidebar");
if (sidebar && sidebar.classList.contains("open")) {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
}
});
});
});