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 ''; }).join(""); wrap.appendChild(menu); var seg = document.createElement("div"); seg.id = "theme-seg"; seg.innerHTML = modes.map(function (m) { return '"; }).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 = '" + (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 = '" + '
' + Array.prototype.map.call(tabs, function (t) { return '" + esc(t.textContent.trim()) + icon("check") + ""; }).join("") + "
"; 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"); } }); }); });