// 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 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();