blog / script.js
Arena Agent
Transfer Members style to blog
9fecd7b
Raw
History Blame Contribute Delete
30.4 kB
(() => {
// ---- Adaptive ambient quality.
// A lightweight requestAnimationFrame probe samples fps in the background.
// If the scene drops below target for a couple of windows, it peels back the
// cheapest-to-lose space effects first (shooting stars, then star count, then
// twinkle) and retires once there is nothing else to cut. ----
function createAutoPerf(opts = {}) {
const prefersReducedMotion = opts.prefersReducedMotion;
const actions = []; // { order, label, run, done }
let started = false;
// order = cut priority (lowest first); cheaper-to-lose / most-expensive-to-run
// effects get the lowest numbers so they go first.
function register(order, label, run) {
actions.push({ order, label, run, done: false });
}
function degradeOnce() {
actions.sort((a, b) => a.order - b.order);
const next = actions.find((a) => !a.done);
if (!next) return false;
next.done = true;
try {
next.run();
} catch (e) {
/* a single failed cut shouldn't stall the rest of the ladder */
}
document.documentElement.dataset.perfCuts = String(actions.filter((a) => a.done).length);
return actions.some((a) => !a.done); // any cuts left?
}
function start(cfg = {}) {
if (started) return;
started = true;
// A page that respects reduced-motion isn't running the heavy ambient
// animation in the first place, so there's nothing to claw back — skip the
// probe entirely rather than burn battery sampling.
if (prefersReducedMotion) return;
const THRESHOLD = cfg.threshold ?? 30; // fps floor we try to hold
const SAMPLE_MS = cfg.sampleMs ?? 1000; // measurement window length
const PATIENCE = cfg.patience ?? 2; // bad windows in a row before cutting
const GRACE_MS = cfg.graceMs ?? 1500; // ignore initial load/layout jank
const COOLDOWN = cfg.cooldownMs ?? 1200; // settle time after each cut
const MAX_LIFE = cfg.maxLifeMs ?? 60000; // stop probing once smooth this long
let frames = 0;
let windowStart = 0;
let holdUntil = 0;
let bornAt = 0;
let lowStreak = 0;
const tick = (now) => {
frames++;
const elapsed = now - windowStart;
if (elapsed >= SAMPLE_MS) {
const fps = (frames * 1000) / elapsed;
frames = 0;
windowStart = now;
// A window that spans a tab switch / long stall (rAF pauses while the
// tab is hidden) reports a meaningless fps — never cut on it.
const stalled = elapsed > SAMPLE_MS * 2.5;
if (!stalled && now >= holdUntil) {
if (fps < THRESHOLD) {
if (++lowStreak >= PATIENCE) {
lowStreak = 0;
const more = degradeOnce();
holdUntil = now + COOLDOWN; // let the cut land before re-judging
if (!more) return; // exhausted the ladder — let the probe die
}
} else {
lowStreak = 0;
if (now - bornAt > MAX_LIFE) return; // smooth long enough — retire
}
}
}
requestAnimationFrame(tick);
};
// Only begin sampling after the grace period so first-paint jank doesn't
// trip an instant downgrade.
setTimeout(() => {
bornAt = windowStart = holdUntil = performance.now();
requestAnimationFrame(tick);
}, GRACE_MS);
}
return { register, start, degradeOnce, actions };
}
document.addEventListener("DOMContentLoaded", () => {
const bg = document.querySelector(".background");
// ---- Detect mobile / low-power devices ----
const isMobile = window.innerWidth < 768 ||
/Android|iPhone|iPad|iPod|webOS|BlackBerry|Opera Mini/i.test(navigator.userAgent);
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// Governs the adaptive downgrade ladder below. The space background
// registers individual cuts as it builds; autoPerf.start() begins watching
// the frame rate once it's up.
const autoPerf = createAutoPerf({ prefersReducedMotion });
window.__autoPerf = autoPerf; // handy for debugging / manual autoPerf.degradeOnce()
// ---- Members-style space background: gradient + lightweight star field ----
if (bg && !bg.querySelector(".star")) {
bg.setAttribute("aria-hidden", "true");
// Cheap: absolutely-positioned dots with opacity-only twinkle.
// This mirrors the Members Space background while keeping the blog fast.
const starCount = isMobile ? 60 : 140;
const frag = document.createDocumentFragment();
for (let i = 0; i < starCount; i++) {
const s = document.createElement("span");
s.className = "star" + (Math.random() < 0.15 ? " big" : "");
s.style.left = Math.random() * 100 + "%";
s.style.top = Math.random() * 100 + "%";
s.style.setProperty("--dur", (3 + Math.random() * 5).toFixed(2) + "s");
s.style.setProperty("--delay", (-Math.random() * 6).toFixed(2) + "s");
s.style.setProperty("--min-op", (0.08 + Math.random() * 0.15).toFixed(2));
s.style.setProperty("--max-op", (0.6 + Math.random() * 0.4).toFixed(2));
frag.appendChild(s);
}
bg.appendChild(frag);
// A few slow shooting stars, skipped on mobile / reduced motion.
if (!isMobile && !prefersReducedMotion) {
for (let i = 0; i < 3; i++) {
const sh = document.createElement("span");
sh.className = "shooting-star";
sh.style.setProperty("--top", (5 + Math.random() * 40) + "%");
sh.style.setProperty("--delay", (i * 7 + Math.random() * 5).toFixed(2) + "s");
bg.appendChild(sh);
}
}
autoPerf.register(10, "space: remove shooting stars", () => {
bg.querySelectorAll(".shooting-star").forEach((s) => s.remove());
});
autoPerf.register(30, "space: thin stars", () => {
bg.querySelectorAll(".star").forEach((s, i) => { if (i % 2 === 0) s.remove(); });
});
autoPerf.register(60, "space: freeze twinkle", () => {
bg.querySelectorAll(".star").forEach((s) => (s.style.animation = "none"));
});
}
// Everything ambient is built and registered — start watching the frame rate.
autoPerf.start();
// ---- Copy buttons on <pre> ----
document.querySelectorAll("pre").forEach((pre) => {
if (pre.querySelector(".copy-btn")) return;
const code = pre.querySelector("code") || pre;
const btn = document.createElement("button");
btn.className = "copy-btn";
btn.textContent = "Copy";
btn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(code.innerText);
btn.textContent = "Copied!";
setTimeout(() => (btn.textContent = "Copy"), 1200);
} catch {
btn.textContent = "Error";
setTimeout(() => (btn.textContent = "Copy"), 1200);
}
});
pre.appendChild(btn);
});
// ---- Share button: copies the canonical ?post= URL for a post ----
const SITE_BASE = "https://huggingface.co/spaces/bench-labs/blog";
const SHARE_ICON =
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<circle cx="18" cy="5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="19" r="2.5"/>' +
'<line x1="8.2" y1="10.7" x2="15.8" y2="6.3"/><line x1="8.2" y1="13.3" x2="15.8" y2="17.7"/>' +
"</svg>";
const CHECK_ICON =
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">' +
'<polyline points="20 6 9 17 4 12"/></svg>';
// A single share menu is open at a time; clicking elsewhere or Escape closes it.
let activeShareMenu = null;
function closeShareMenu() {
if (activeShareMenu) { activeShareMenu.remove(); activeShareMenu = null; }
}
document.addEventListener("click", closeShareMenu);
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeShareMenu(); });
window.addEventListener("resize", closeShareMenu);
window.addEventListener("scroll", closeShareMenu, true);
function flashCopied(btn, ok) {
btn.classList.remove("copied", "error");
btn.classList.add(ok ? "copied" : "error");
if (ok) btn.innerHTML = CHECK_ICON;
btn.dataset.tooltip = ok ? "Copied!" : "Couldn't copy";
setTimeout(() => {
btn.classList.remove("copied", "error");
btn.innerHTML = SHARE_ICON;
btn.dataset.tooltip = "Share";
}, 1400);
}
// Build the three shareable representations of a post.
function shareFormats(filename, meta) {
const url = `${SITE_BASE}?post=${filename}`;
const title = (meta.title || filename).replace(/\s+/g, " ").trim();
const desc = (meta.description || "").replace(/\s+/g, " ").trim();
return [
{ label: "Link", sub: "URL only", text: url },
{ label: "Markdown", sub: "[title](link)", text: `[${title}](${url})` },
{ label: "Markdown + description", sub: "title, link & summary",
text: desc ? `[${title}](${url})\n\n${desc}` : `[${title}](${url})` },
];
}
function openShareMenu(btn, filename, getMeta) {
closeShareMenu();
const menu = document.createElement("div");
menu.className = "share-menu";
menu.setAttribute("role", "menu");
const heading = document.createElement("div");
heading.className = "share-menu-title";
heading.textContent = "Copy as";
menu.appendChild(heading);
shareFormats(filename, getMeta()).forEach((fmt) => {
const item = document.createElement("button");
item.type = "button";
item.className = "share-menu-item";
item.setAttribute("role", "menuitem");
item.innerHTML = `<span>${fmt.label}</span><span class="sub">${fmt.sub}</span>`;
item.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
closeShareMenu();
try {
await navigator.clipboard.writeText(fmt.text);
flashCopied(btn, true);
} catch {
flashCopied(btn, false);
}
});
menu.appendChild(item);
});
// Position under the button, right-aligned, clamped to the viewport.
document.body.appendChild(menu);
const r = btn.getBoundingClientRect();
const mw = menu.offsetWidth, mh = menu.offsetHeight, gap = 8, pad = 8;
let left = Math.min(r.right - mw, window.innerWidth - mw - pad);
left = Math.max(pad, left);
let top = r.bottom + gap;
if (top + mh > window.innerHeight - pad) top = Math.max(pad, r.top - gap - mh);
menu.style.left = `${Math.round(left)}px`;
menu.style.top = `${Math.round(top)}px`;
menu.addEventListener("click", (e) => e.stopPropagation());
activeShareMenu = menu;
}
function makeShareBtn(filename, getMeta) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "share-btn tooltip";
btn.setAttribute("aria-label", "Share");
btn.setAttribute("aria-haspopup", "menu");
btn.dataset.tooltip = "Share";
btn.innerHTML = SHARE_ICON;
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (activeShareMenu) { closeShareMenu(); return; }
openShareMenu(btn, filename, getMeta || (() => ({ title: filename, description: "" })));
});
return btn;
}
// One share button per post card on the index page
document.querySelectorAll(".post-card").forEach((card) => {
if (card.querySelector(".share-btn")) return;
const link = card.querySelector('a[href^="posts/"]');
if (!link) return;
const filename = link.getAttribute("href").split("/").pop();
card.appendChild(makeShareBtn(filename, () => ({
title: card.querySelector("h2")?.textContent || filename,
description: card.querySelector("p:not(.post-card-snippet)")?.textContent || "",
})));
});
// ---- Search bar (index page only): basic title/description filter,
// deep search across fetched post content, and a date-sort filter popup ----
const postsSection = document.querySelector(".posts");
if (postsSection) {
const form = document.getElementById("search-form");
const input = document.getElementById("search-input");
const deepBtn = document.getElementById("deep-search-btn");
const filterBtn = document.getElementById("filter-btn");
const filterPopup = document.getElementById("filter-popup");
const statusEl = document.getElementById("deep-search-status");
const cardData = Array.from(postsSection.querySelectorAll(".post-card")).map((card) => {
const link = card.querySelector('a[href^="posts/"]');
return {
card,
file: link ? link.getAttribute("href").split("/").pop() : null,
title: (card.querySelector("h2")?.textContent || "").trim(),
desc: (card.querySelector("p")?.textContent || "").trim(),
dateText: (card.querySelector(".date")?.textContent || "").trim(),
};
});
const deepTextCache = new Map(); // filename -> plain text content of that post
function clearSnippets() {
cardData.forEach(({ card }) => card.querySelector(".post-card-snippet")?.remove());
}
function showAll() {
clearSnippets();
cardData.forEach(({ card }) => (card.style.display = ""));
}
function runBasicSearch(query) {
const q = query.trim().toLowerCase();
clearSnippets();
if (!q) {
cardData.forEach(({ card }) => (card.style.display = ""));
return;
}
cardData.forEach(({ card, title, desc }) => {
const match = title.toLowerCase().includes(q) || desc.toLowerCase().includes(q);
card.style.display = match ? "" : "none";
});
}
async function fetchPostText(file) {
if (deepTextCache.has(file)) return deepTextCache.get(file);
const res = await fetch("posts/" + file);
const html = await res.text();
const doc = new DOMParser().parseFromString(html, "text/html");
const text = (doc.querySelector(".article") || doc.body).textContent.replace(/\s+/g, " ").trim();
deepTextCache.set(file, text);
return text;
}
function escapeHtml(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function makeSnippet(text, q) {
const idx = text.toLowerCase().indexOf(q);
if (idx === -1) return null;
const start = Math.max(0, idx - 50);
const end = Math.min(text.length, idx + q.length + 70);
let snippet = text.slice(start, end);
if (start > 0) snippet = "…" + snippet;
if (end < text.length) snippet += "…";
return snippet;
}
function highlightSnippet(snippet, q) {
const idx = snippet.toLowerCase().indexOf(q.toLowerCase());
if (idx === -1) return escapeHtml(snippet);
return (
escapeHtml(snippet.slice(0, idx)) +
"<mark>" + escapeHtml(snippet.slice(idx, idx + q.length)) + "</mark>" +
escapeHtml(snippet.slice(idx + q.length))
);
}
async function runDeepSearch(query) {
const q = query.trim().toLowerCase();
clearSnippets();
if (!q) {
showAll();
return;
}
deepBtn.classList.add("is-busy");
deepBtn.disabled = true;
statusEl.hidden = false;
statusEl.textContent = "Searching " + cardData.length + " posts…";
try {
await Promise.all(
cardData.map(async ({ card, title, desc, file }) => {
const titleHit = title.toLowerCase().includes(q);
const descHit = desc.toLowerCase().includes(q);
let snippet = null;
if (file) {
try {
const text = await fetchPostText(file);
snippet = makeSnippet(text, q);
} catch {
// couldn't fetch this post's content — fall back to title/description match only
}
}
const match = titleHit || descHit || snippet;
card.style.display = match ? "" : "none";
if (match && snippet) {
const snippetEl = document.createElement("p");
snippetEl.className = "post-card-snippet";
snippetEl.innerHTML = "Found in post: " + highlightSnippet(snippet, q);
const readLink = card.querySelector('a[href^="posts/"]');
if (readLink) readLink.insertAdjacentElement("beforebegin", snippetEl);
else card.appendChild(snippetEl);
}
})
);
} finally {
deepBtn.classList.remove("is-busy");
deepBtn.disabled = false;
statusEl.hidden = true;
}
}
form?.addEventListener("submit", (e) => {
e.preventDefault();
runBasicSearch(input.value);
});
deepBtn?.addEventListener("click", () => {
runDeepSearch(input.value);
});
// ---- Filter popup: sort by date ----
function parseCardDate(dateText) {
const m = dateText.match(/^(\w+)\s+(\d{4})$/);
if (!m) return null;
const t = new Date(`${m[1]} 1, ${m[2]}`).getTime();
return Number.isNaN(t) ? null : t;
}
function applySort(direction) {
const withDate = cardData.filter((c) => parseCardDate(c.dateText) !== null);
const withoutDate = cardData.filter((c) => parseCardDate(c.dateText) === null);
withDate.sort((a, b) => {
const ta = parseCardDate(a.dateText);
const tb = parseCardDate(b.dateText);
return direction === "new" ? tb - ta : ta - tb;
});
[...withDate, ...withoutDate].forEach(({ card }) => postsSection.appendChild(card));
}
filterBtn?.addEventListener("click", () => {
const opening = filterPopup.hidden;
filterPopup.hidden = !opening;
filterBtn.setAttribute("aria-expanded", String(opening));
});
filterPopup?.querySelectorAll(".filter-choice").forEach((btn) => {
btn.addEventListener("click", () => {
filterPopup.querySelectorAll(".filter-choice").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
applySort(btn.dataset.sort);
});
});
document.addEventListener("click", (e) => {
if (filterPopup && !filterPopup.hidden && !filterPopup.contains(e.target) && e.target !== filterBtn && !filterBtn.contains(e.target)) {
filterPopup.hidden = true;
filterBtn.setAttribute("aria-expanded", "false");
}
});
}
// ---- Article-only features: sidebar + collapsible code ----
const article = document.querySelector(".article");
if (!article) return;
// ---- Wrap tables so very wide ones scroll horizontally instead of
// overflowing the page. The table stays width:100% (a real table, so its
// columns distribute across the full container); the wrapper owns the
// overflow-x scrolling. ----
article.querySelectorAll("table").forEach((table) => {
if (table.parentElement.classList.contains("table-scroll")) return;
const wrap = document.createElement("div");
wrap.className = "table-scroll";
table.parentNode.insertBefore(wrap, table);
wrap.appendChild(table);
});
// ---- Share button for the current post ----
{
const filename = location.pathname.split("/").pop();
if (filename && /\.html?$/i.test(filename) && !article.querySelector(".share-btn")) {
const shareBtn = makeShareBtn(filename, () => ({
title: article.querySelector("h1")?.textContent || document.title,
description: article.querySelector("h1 ~ p")?.textContent
|| article.querySelector("p")?.textContent || "",
}));
shareBtn.classList.add("share-btn-article");
const back = article.querySelector(".back");
if (back) back.insertAdjacentElement("afterend", shareBtn);
else article.prepend(shareBtn);
}
}
// ---- External links: open in a new tab + hover preview card with fetched metadata ----
{
const linkMetaCache = new Map();
function faviconlessTitle(href) {
try {
const u = new URL(href);
const parts = u.pathname.split("/").filter(Boolean);
if (u.hostname === "huggingface.co" && parts[0] === "spaces" && parts.length >= 3) {
return `${parts[1]}/${parts[2]} · Hugging Face Space`;
}
if (u.hostname === "huggingface.co" && parts.length >= 2) {
return `${parts[0]}/${parts[1]} · Hugging Face Hub`;
}
return u.hostname;
} catch {
return href;
}
}
async function loadLinkMeta(href) {
if (linkMetaCache.has(href)) return linkMetaCache.get(href);
const metaPromise = (async () => {
try {
const res = await fetch(href, { mode: "cors" });
if (!res.ok) throw new Error("bad status");
const html = await res.text();
const doc = new DOMParser().parseFromString(html, "text/html");
const title = doc.querySelector("title")?.textContent?.trim() || null;
const desc =
doc.querySelector('meta[name="description"]')?.getAttribute("content")?.trim() ||
doc.querySelector('meta[property="og:description"]')?.getAttribute("content")?.trim() ||
null;
return { title, desc };
} catch {
return { title: null, desc: null };
}
})();
linkMetaCache.set(href, metaPromise);
return metaPromise;
}
article.querySelectorAll('a[href^="http"]').forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
window.open(link.href, "_blank", "noopener,noreferrer");
});
let card = null;
let hideTimer = null;
link.addEventListener("mouseenter", () => {
clearTimeout(hideTimer);
card = document.createElement("div");
card.className = "link-preview-card";
const urlEl = document.createElement("div");
urlEl.className = "link-preview-url";
urlEl.textContent = link.href;
const titleEl = document.createElement("div");
titleEl.className = "link-preview-title";
titleEl.textContent = faviconlessTitle(link.href);
card.appendChild(titleEl);
card.appendChild(urlEl);
document.body.appendChild(card);
const rect = link.getBoundingClientRect();
card.style.top = rect.bottom + window.scrollY + 8 + "px";
const maxLeft = window.scrollX + document.documentElement.clientWidth - card.offsetWidth - 12;
card.style.left = Math.min(rect.left + window.scrollX, Math.max(12, maxLeft)) + "px";
requestAnimationFrame(() => card?.classList.add("visible"));
loadLinkMeta(link.href).then((meta) => {
if (!card || !card.isConnected) return;
if (meta.title) titleEl.textContent = meta.title;
if (meta.desc) {
const descEl = document.createElement("div");
descEl.className = "link-preview-desc";
descEl.textContent = meta.desc;
card.appendChild(descEl);
}
});
});
link.addEventListener("mouseleave", () => {
const toRemove = card;
card = null;
if (!toRemove) return;
toRemove.classList.remove("visible");
hideTimer = setTimeout(() => toRemove.remove(), 180);
});
});
}
const headings = article.querySelectorAll("h1, h2, h3");
const codeBlocks = article.querySelectorAll("pre");
const codeFiles = [];
codeBlocks.forEach((pre, idx) => {
let name = null;
let el = pre.previousElementSibling;
while (el) {
if (el.tagName === "H2" || el.tagName === "H3") {
const text = el.textContent.trim();
if (text.match(/\.\w{1,5}$/) || text.match(/\.\w{1,5}\s/)) {
name = text.replace(/\s*\(.*\)/, "").trim();
}
break;
}
el = el.previousElementSibling;
}
if (!name) name = `code-block-${idx + 1}.txt`;
codeFiles.push({ name, pre });
});
// Make code blocks collapsible
codeBlocks.forEach((pre, idx) => {
const wrapper = document.createElement("div");
wrapper.className = "code-collapsible";
const toggle = document.createElement("button");
toggle.className = "code-toggle";
const fileName = codeFiles[idx].name;
toggle.innerHTML = `<span class="code-toggle-arrow">▶</span> <span class="code-toggle-name">${fileName}</span>`;
toggle.setAttribute("aria-expanded", "false");
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(toggle);
wrapper.appendChild(pre);
pre.classList.add("collapsed");
toggle.addEventListener("click", () => {
const expanded = pre.classList.toggle("collapsed");
toggle.setAttribute("aria-expanded", !expanded);
toggle.querySelector(".code-toggle-arrow").textContent = expanded ? "▶" : "▼";
});
});
// ---- Build sidebar ----
// MOBILE: sidebar becomes a collapsible drawer at the top
const sidebar = document.createElement("nav");
sidebar.className = "article-sidebar";
if (isMobile) {
sidebar.classList.add("sidebar-mobile");
const drawerToggle = document.createElement("button");
drawerToggle.className = "sidebar-drawer-toggle";
drawerToggle.textContent = "☰ Contents & Files";
drawerToggle.setAttribute("aria-expanded", "false");
const drawerBody = document.createElement("div");
drawerBody.className = "sidebar-drawer-body";
drawerBody.style.display = "none";
drawerToggle.addEventListener("click", () => {
const open = drawerBody.style.display === "none";
drawerBody.style.display = open ? "block" : "none";
drawerToggle.setAttribute("aria-expanded", open);
});
sidebar.appendChild(drawerToggle);
sidebar.appendChild(drawerBody);
// We'll append TOC / files into drawerBody instead
sidebar._container = drawerBody;
} else {
sidebar._container = sidebar;
}
const container = sidebar._container;
// TOC section
const tocTitle = document.createElement("div");
tocTitle.className = "sidebar-title";
tocTitle.textContent = "Contents";
container.appendChild(tocTitle);
const tocList = document.createElement("ul");
tocList.className = "sidebar-toc";
headings.forEach((h, i) => {
if (!h.id) h.id = "heading-" + i;
const li = document.createElement("li");
li.className = "toc-" + h.tagName.toLowerCase();
const a = document.createElement("a");
a.href = "#" + h.id;
a.textContent = h.textContent;
a.addEventListener("click", (e) => {
e.preventDefault();
h.scrollIntoView({ behavior: "smooth", block: "start" });
// MOBILE: close drawer after tap
if (isMobile) {
const body = sidebar.querySelector(".sidebar-drawer-body");
if (body) body.style.display = "none";
const tog = sidebar.querySelector(".sidebar-drawer-toggle");
if (tog) tog.setAttribute("aria-expanded", "false");
}
});
li.appendChild(a);
tocList.appendChild(li);
});
container.appendChild(tocList);
// Files section
if (codeFiles.length > 0) {
const filesTitle = document.createElement("div");
filesTitle.className = "sidebar-title";
filesTitle.textContent = "Files";
container.appendChild(filesTitle);
const fileList = document.createElement("ul");
fileList.className = "sidebar-files";
codeFiles.forEach(({ name, pre }) => {
const li = document.createElement("li");
const btn = document.createElement("button");
btn.className = "sidebar-file-btn";
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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"/></svg> ${name}`;
btn.addEventListener("click", () => {
const code = pre.querySelector("code") || pre;
const text = code.innerText;
const blob = new Blob([text], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = name;
a.click();
URL.revokeObjectURL(url);
});
li.appendChild(btn);
fileList.appendChild(li);
});
container.appendChild(fileList);
}
document.body.appendChild(sidebar);
// Highlight active TOC item on scroll
// MOBILE: use a throttled observer with larger margins for perf
const tocLinks = tocList.querySelectorAll("a");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
tocLinks.forEach((l) => l.classList.remove("active"));
const link = tocList.querySelector(`a[href="#${entry.target.id}"]`);
if (link) link.classList.add("active");
}
});
},
{
rootMargin: isMobile ? "-60px 0px -60% 0px" : "-80px 0px -70% 0px",
// MOBILE: lower threshold = fewer callbacks
threshold: isMobile ? 0 : 0,
}
);
headings.forEach((h) => observer.observe(h));
});
})();