Initial documentation

This commit is contained in:
2026-10-06 21:47:41 -07:00
commit b6eb41dfdc
51 changed files with 10351 additions and 0 deletions

653
assets/app.js Normal file
View File

@@ -0,0 +1,653 @@
var THEME_KEY = "vyla-theme";
var ICONS = {
system: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>',
light: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4"/>',
dark: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
search: '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>',
menu: '<path d="M4 6h16M4 12h10M4 18h16"/>',
doc: '<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5M9 13h6M9 17h6"/>',
hash: '<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18"/>',
copy: '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/>',
check: '<path d="m5 12 5 5 9-10"/>',
chevron: '<path d="m6 9 6 6 6-6"/>'
};
var searchData = null;
var searchResults = [];
var searchActive = 0;
function icon(name) {
return '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + ICONS[name] + "</svg>";
}
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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 '<button type="button" data-mode="' + m + '">' + icon(m) + "<span>" + m.charAt(0).toUpperCase() + m.slice(1) + '</span><i class="check">✓</i></button>';
}).join("");
wrap.appendChild(menu);
var seg = document.createElement("div");
seg.id = "theme-seg";
seg.innerHTML = modes.map(function (m) {
return '<button type="button" data-mode="' + m + '" aria-label="' + m + '">' + icon(m) + "</button>";
}).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 =
'<button id="mobile-toggle" type="button" aria-label="Toggle navigation" onclick="toggleSidebar()">' + icon("menu") + "</button>" +
(group ? "<span>" + esc(group) + '</span><span class="sep">›</span>' : "") +
'<span class="crumb-current">' + esc(a ? a.textContent.trim() : document.title) + "</span>";
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 =
'<button type="button" class="mt-btn" aria-expanded="false"><span>' + esc(currentTab.textContent.trim()) + "</span>" + icon("chevron") + "</button>" +
'<div class="mt-menu">' + Array.prototype.map.call(tabs, function (t) {
return '<a href="' + t.getAttribute("href") + '"' + (t === currentTab ? ' class="on"' : "") + ">" + esc(t.textContent.trim()) + icon("check") + "</a>";
}).join("") + "</div>";
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 ? '<b class="hit">' + esc(part) + "</b>" : 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 = '<div class="search-label">Indexing pages...</div>';
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 = '<div class="search-label">' + (tokens.length ? "No results found" : "No recent searches") + "</div>";
return;
}
box.innerHTML = (tokens.length ? "" : '<div class="search-label">Recent searches</div>') + searchResults.map(function (it, i) {
var snippet = tokens.length ? makeSnippet(it, tokens) : "";
var crumbs = it.crumbs.map(function (c) { return hl(c, tokens); }).join('<span class="sep">›</span>');
return '<a class="sr' + (i === searchActive ? " on" : "") + '" href="' + esc(it.href) + '" data-i="' + i + '">' +
icon(it.type === "heading" ? "hash" : "doc") +
'<div class="sr-body"><div class="t">' + esc(it.title) + "</div>" +
(snippet ? '<div class="s">' + hl(snippet, tokens) + "</div>" : "") +
'<div class="p">' + crumbs + "</div></div></a>";
}).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") + "<span>Search...</span><kbd>Ctrl K</kbd>";
var modal = document.createElement("div");
modal.id = "search-modal";
modal.innerHTML =
'<div class="search-backdrop"></div><div class="search-panel"><div class="search-input-row">' + icon("search") +
'<input id="search-input" type="text" placeholder="Search..." autocomplete="off" spellcheck="false"></div>' +
'<div id="search-results"></div><div class="search-footer"><span><kbd>↑</kbd><kbd>↓</kbd>Select</span><span><kbd>↵</kbd>Open</span><span class="close-hint"><kbd>esc</kbd>Close</span></div></div>';
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");
}
});
});
});