// Kepr's browser islands (§18). Every page is complete without this file; it only adds the // styled timeline tooltip, copy buttons, line-range highlighting and the Atlas panel and filter. function timelineTips() { const dots = document.querySelectorAll(".timeline .dot"); if (dots.length === 0) return; const tip = document.createElement("div"); tip.className = "timeline-tip"; tip.hidden = true; document.body.append(tip); for (const dot of dots) { // The SVG is the no-JavaScript tooltip; lift it out so the two never show together. const title = dot.querySelector("title"); const [id, message, meta] = (title?.textContent ?? "").split("\n"); title?.remove(); dot.setAttribute("aria-label", `${id} ${message}`); dot.addEventListener("pointerenter", (event) => { tip.replaceChildren(line("tip-id", id), line("tip-msg", message), line("tip-meta", meta)); tip.hidden = false; const left = event.clientX + 14; tip.style.left = `${left + 340 > window.innerWidth ? Math.max(8, event.clientX - 354) : left}px`; tip.style.top = `${event.clientY - 10}px`; }); dot.addEventListener("pointerleave", () => { tip.hidden = true; }); } } function line(className, text) { const div = document.createElement("div"); div.className = className; div.textContent = text ?? ""; return div; } function copyButtons() { for (const button of document.querySelectorAll(".copy-btn[data-copy]")) { button.hidden = false; button.addEventListener("click", async () => { await navigator.clipboard.writeText(button.dataset.copy); button.textContent = "copied"; button.classList.add("copied"); setTimeout(() => { button.textContent = "copy"; button.classList.remove("copied"); }, 1400); }); } } // `#L42` works through CSS :target alone; a range `#L42-L50` needs marking line by line. function lineRanges() { const code = document.querySelector(".code"); if (!code) return; let anchor = null; const mark = () => { for (const marked of code.querySelectorAll(".line.hl")) marked.classList.remove("hl"); const match = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash); if (!match) return; const from = Number(match[1]); const to = Number(match[2] ?? match[1]); anchor = from; for (let n = Math.min(from, to); n <= Math.max(from, to); n++) { document.getElementById(`L${n}`)?.classList.add("hl"); } document.getElementById(`L${Math.min(from, to)}`)?.scrollIntoView({ block: "center" }); }; code.addEventListener("click", (event) => { const link = event.target.closest(".ln"); if (!link || !event.shiftKey || anchor === null) return; event.preventDefault(); const n = Number(link.textContent); history.replaceState(null, "", `#L${Math.min(anchor, n)}-L${Math.max(anchor, n)}`); mark(); }); window.addEventListener("hashchange", mark); mark(); } // The Atlas (§21.5): each row's detail panel waits in the <template> after it; the filter narrows // by filename; which folders are open is remembered per repo in this browser. function atlas() { const root = document.querySelector(".atlas"); const filter = document.querySelector(".atlas-filter"); if (!root || !filter) return; const panel = root.querySelector(".atlas-panel"); const folders = [...root.querySelectorAll(".atlas-folder")]; const files = [...root.querySelectorAll(".atlas-file")]; const key = `kepr:atlas:${root.dataset.repo}`; const opened = new Set(remembered(key)); for (const folder of folders) { folder.open = opened.has(folder.dataset.path); folder.addEventListener("toggle", () => { if (filter.value.trim()) return; if (folder.open) opened.add(folder.dataset.path); else opened.delete(folder.dataset.path); remember(key, [...opened]); }); } let selected = null; const show = (row) => { selected?.classList.remove("selected"); selected = row; row.classList.add("selected"); panel.replaceChildren(row.nextElementSibling.content.cloneNode(true)); }; for (const row of root.querySelectorAll(".atlas-row")) { row.addEventListener("pointerenter", (event) => { if (event.pointerType === "mouse") show(row); }); row.addEventListener("focus", () => show(row)); } filter.addEventListener("input", () => { const query = filter.value.trim().toLowerCase(); for (const file of files) file.hidden = query !== "" && !file.dataset.name.includes(query); for (const folder of folders) { folder.hidden = query !== "" && !folder.querySelector(".atlas-file:not([hidden])"); folder.open = query !== "" || opened.has(folder.dataset.path); } }); filter.hidden = false; panel.hidden = false; } // Storage can be missing or refused (private windows, blocked site data); the tree then starts closed. function remembered(key) { try { return JSON.parse(localStorage.getItem(key) ?? "[]"); } catch { return []; } } function remember(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch {} } timelineTips(); copyButtons(); lineRanges(); atlas();