1234567const KINDS = [8 [/\b(release|version|bump|tag)\b/i, "accent", 1],9 [/\b(fix|bug|patch|issue|crash|error|regress|revert)\b/i, "accent-dim", 0.8],10 [/\b(add|new|implement|feat|introduce|wire|support)\b/i, "accent", 1],11 [12 /\b(refactor|restructure|reorganize|extract|move|rename|split|cleanup|clean up)\b/i,13 "text",14 0.85,15 ],16 [/\b(perf|optim|speed|fast|slow|cache|memory|alloc)\b/i, "accent", 0.9],17 [/\b(test|spec|coverage|assert)\b/i, "text-secondary", 0.74],18 [/\b(doc|readme|comment|example|guide)\b/i, "text-secondary", 0.7],19 [/\b(polish|style|format|lint|whitespace|typo|wording|copy)\b/i, "text-secondary", 0.66],20 [/\b(chore|deps|update|upgrade|build|ci|config)\b/i, "text-secondary", 0.62],21];22const DAY = 86400;2324252627function timeline() {28 const svg = document.querySelector("svg.timeline");29 if (!svg) return;30 const css = getComputedStyle(document.documentElement);31 const saves = [...svg.querySelectorAll(".dot")]32 .map((dot) => {33 const [id, message, meta] = (dot.querySelector("title")?.textContent ?? "").split("\n");34 const [, color, alpha] = KINDS.find(([kind]) => kind.test(message)) ?? [35 null,36 "text-secondary",37 0.8,38 ];39 const fill = css.getPropertyValue(`--${color}`).trim();40 return { href: dot.getAttribute("href"), id, message, meta, fill, alpha, t: +dot.dataset.t };41 })42 .sort((a, b) => a.t - b.t);43 if (saves.length === 0) return;44 const canvas = document.createElement("canvas");45 canvas.className = "timeline-canvas";46 canvas.setAttribute("role", "img");47 canvas.setAttribute("aria-label", svg.getAttribute("aria-label"));48 svg.replaceWith(canvas);49 const ctx = canvas.getContext("2d");50 const tip = document.createElement("div");51 tip.className = "timeline-tip";52 tip.hidden = true;53 document.body.append(tip);54 const reduce = matchMedia("(prefers-reduced-motion: reduce)");55 const [first, last] = [saves[0].t, saves[saves.length - 1].t];56 const end = Math.max(Date.now() / 1000, last) + DAY;57 const start = Math.min(first, Date.now() / 1000) - Math.max(0, 7 * DAY - (end - first));58 let [width, height, strip, radius, offset] = [0, 0, 0, 0, 0];59 const xOf = (t) => (strip * (t - start)) / (end - start);60 const resize = () => {61 width = canvas.parentElement.getBoundingClientRect().width;62 height = Math.min(320, Math.max(120, 120 + Math.floor(Math.log2(saves.length + 1) * 30)));63 canvas.width = Math.round(width * devicePixelRatio);64 canvas.height = Math.round(height * devicePixelRatio);65 canvas.style.width = `${width}px`;66 canvas.style.height = `${height}px`;67 ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);68 strip = Math.max(width * 3, saves.length * 48);69 radius = Math.min(5, height * 0.022);70 let previous = Number.NEGATIVE_INFINITY;71 for (const save of saves) {72 save.x = Math.max(xOf(save.t), previous + radius + 8);73 previous = save.x;74 }75 offset = Math.max(0, previous - width * 0.82);76 };77 78 const onScreen = (x, margin) => {79 const shift = ((offset % strip) + strip) % strip;80 return [-1, 0, 1]81 .map((k) => x - shift + k * strip)82 .filter((sx) => sx > -margin && sx < width + margin);83 };84 const hit = (event) => {85 const box = canvas.getBoundingClientRect();86 const [mx, my] = [event.clientX - box.left, event.clientY - box.top];87 if (Math.abs(my - height / 2) > radius + 7) return null;88 return (89 saves.find((save) => onScreen(save.x, 0).some((x) => Math.abs(mx - x) < radius + 7)) ?? null90 );91 };92 let [hovered, drag, dragFrom, downX, lastX, velocity, resumeAt, lastFrame] = [93 null,94 null,95 0,96 0,97 0,98 0,99 0,100 0,101 ];102 const hover = (save, event) => {103 if (save !== hovered) {104 hovered = save;105 tip.hidden = save === null;106 if (save)107 tip.replaceChildren(108 line("tip-id", save.id),109 line("tip-msg", save.message),110 line("tip-meta", save.meta),111 );112 }113 if (save) {114 const left = event.clientX + 14;115 tip.style.left = `${left + 340 > innerWidth ? Math.max(8, event.clientX - 354) : left}px`;116 tip.style.top = `${event.clientY - 10}px`;117 }118 canvas.style.cursor = save ? "pointer" : drag === null ? "grab" : "grabbing";119 };120 const draw = (time) => {121 const dt = lastFrame ? Math.min((time - lastFrame) / 1000, 0.1) : 0;122 lastFrame = time;123 if (drag === null && hovered === null) {124 if (Math.abs(velocity) > 0.5) {125 offset -= velocity;126 velocity *= 0.92;127 } else if (!reduce.matches && time >= resumeAt) offset += 20 * dt;128 }129 const y = height / 2;130 ctx.clearRect(0, 0, width, height);131 ctx.lineWidth = 1;132 ctx.strokeStyle = css.getPropertyValue("--border");133 const years = [new Date(start * 1000).getFullYear(), new Date(end * 1000).getFullYear()];134 for (let year = years[0]; year <= years[1]; year++) {135 for (const x of onScreen(xOf(new Date(year, 0, 1) / 1000), 0)) {136 ctx.beginPath();137 ctx.moveTo(x, height * 0.1);138 ctx.lineTo(x, height * 0.9);139 ctx.stroke();140 }141 }142 ctx.globalAlpha = 0.7;143 ctx.strokeStyle = css.getPropertyValue("--text-muted");144 ctx.setLineDash([4, 4]);145 ctx.beginPath();146 ctx.moveTo(0, y);147 ctx.lineTo(width, y);148 ctx.stroke();149 ctx.setLineDash([]);150 ctx.font = '10px "Martian Mono", monospace';151 ctx.textAlign = "center";152 for (const save of saves) {153 for (const x of onScreen(save.x, 10)) {154 ctx.globalAlpha = hovered === null ? save.alpha : hovered === save ? 1 : save.alpha * 0.6;155 ctx.fillStyle = save.fill;156 ctx.beginPath();157 ctx.arc(x, y, radius, 0, Math.PI * 2);158 ctx.fill();159 if (hovered === save) {160 ctx.fillStyle = css.getPropertyValue("--accent");161 ctx.fillText(save.id.slice(0, 6), x, y - radius - 6);162 }163 }164 }165 ctx.globalAlpha = 1;166 requestAnimationFrame(draw);167 };168 canvas.addEventListener("pointerdown", (event) => {169 [drag, dragFrom, downX, lastX, velocity] = [170 event.clientX,171 offset,172 event.clientX,173 event.clientX,174 0,175 ];176 canvas.setPointerCapture(event.pointerId);177 });178 canvas.addEventListener("pointermove", (event) => {179 if (drag !== null) {180 offset = dragFrom - (event.clientX - drag);181 velocity = velocity * 0.6 + (event.clientX - lastX) * 0.4;182 lastX = event.clientX;183 }184 if (event.pointerType === "mouse") hover(hit(event), event);185 });186 const release = () => {187 drag = null;188 resumeAt = performance.now() + 900;189 };190 canvas.addEventListener("pointerup", (event) => {191 release();192 const save = Math.abs(event.clientX - downX) > 4 ? null : hit(event);193 if (save) location.href = save.href;194 });195 canvas.addEventListener("pointercancel", release);196 canvas.addEventListener("pointerleave", (event) => {197 if (drag === null) hover(null, event);198 });199 addEventListener("resize", resize);200 resize();201 requestAnimationFrame(draw);202}203204function line(className, text) {205 const div = document.createElement("div");206 div.className = className;207 div.textContent = text ?? "";208 return div;209}210211function copyButtons(root) {212 for (const button of root.querySelectorAll("button[data-copy]")) {213 const label = button.textContent;214 button.hidden = false;215 button.addEventListener("click", async () => {216 await navigator.clipboard.writeText(button.dataset.copy);217 button.textContent = "copied";218 button.classList.add("copied");219 setTimeout(() => {220 button.textContent = label;221 button.classList.remove("copied");222 }, 1400);223 });224 }225}226227228229async function fetchMain(url) {230 try {231 const response = await fetch(url);232 if (!response.ok) return null;233 const page = new DOMParser().parseFromString(await response.text(), "text/html");234 const main = page.querySelector("main");235 for (const link of main.querySelectorAll("a[href]:not([href^='#'])")) {236 link.href = new URL(link.getAttribute("href"), url).href;237 }238 return main;239 } catch {240 return null;241 }242}243244245function modified(event) {246 return event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;247}248249250251function panels() {252 const host = document.querySelector(".repo-panel");253 if (!host) return;254 let open = null;255 let wanted = "";256 const load = async (url) => {257 wanted = url;258 const main = await fetchMain(url);259 if (wanted !== url) return;260 if (!main) {261 location.href = url;262 return;263 }264 main.querySelector(".breadcrumb")?.remove();265 main.querySelector(".file-page-title")?.remove();266 host.replaceChildren(...main.childNodes);267 copyButtons(host);268 host.hidden = false;269 };270 for (const tab of document.querySelectorAll(".repo-tab[data-panel]")) {271 tab.addEventListener("click", (event) => {272 if (modified(event)) return;273 event.preventDefault();274 open?.classList.remove("active");275 if (open === tab) {276 open = null;277 wanted = "";278 host.hidden = true;279 return;280 }281 open = tab;282 tab.classList.add("active");283 load(tab.href);284 });285 }286 host.addEventListener("click", (event) => {287 const link = event.target.closest("a[href]");288 if (!link || !open || modified(event)) return;289 if (new URL(link.href).pathname !== new URL(open.href).pathname) return;290 event.preventDefault();291 load(link.href);292 });293}294295296297function saveLists() {298 for (const list of document.querySelectorAll(".save-list")) {299 const observer = new IntersectionObserver(300 async ([entry]) => {301 const older = list.querySelector(".pager-older");302 if (!entry?.isIntersecting || !older) return;303 observer.disconnect();304 const main = await fetchMain(older.href);305 const next = main?.querySelector(`.save-list[data-list="${list.dataset.list}"]`);306 if (!next) return;307 next.querySelector(".pager a:not(.pager-older)")?.remove();308 list.querySelector(".pager").replaceWith(...next.childNodes);309 watch();310 },311 { root: list },312 );313 const watch = () => {314 const older = list.querySelector(".pager-older");315 if (older) observer.observe(older);316 };317 watch();318 }319}320321322function lineRanges() {323 const code = document.querySelector(".code");324 if (!code) return;325 let anchor = null;326 const mark = () => {327 for (const marked of code.querySelectorAll(".line.hl")) marked.classList.remove("hl");328 const match = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash);329 if (!match) return;330 const from = Number(match[1]);331 const to = Number(match[2] ?? match[1]);332 anchor = from;333 for (let n = Math.min(from, to); n <= Math.max(from, to); n++) {334 document.getElementById(`L${n}`)?.classList.add("hl");335 }336 document.getElementById(`L${Math.min(from, to)}`)?.scrollIntoView({ block: "center" });337 };338 code.addEventListener("click", (event) => {339 const link = event.target.closest(".ln");340 if (!link || !event.shiftKey || anchor === null) return;341 event.preventDefault();342 const n = Number(link.textContent);343 history.replaceState(null, "", `#L${Math.min(anchor, n)}-L${Math.max(anchor, n)}`);344 mark();345 });346 window.addEventListener("hashchange", mark);347 mark();348}349350351352function atlas() {353 const root = document.querySelector(".atlas");354 const filter = document.querySelector(".atlas-filter");355 if (!root || !filter) return;356 const panel = root.querySelector(".atlas-panel");357 const folders = [...root.querySelectorAll(".atlas-folder")];358 const files = [...root.querySelectorAll(".atlas-file")];359 const key = `kepr:atlas:${root.dataset.repo}`;360 const opened = new Set(remembered(key));361 362 const unfold = new URLSearchParams(location.search).get("open");363 const onPath = (path) => unfold !== null && `${unfold}/`.startsWith(`${path}/`);364 for (const folder of folders) {365 folder.open = opened.has(folder.dataset.path) || onPath(folder.dataset.path);366 folder.addEventListener("toggle", () => {367 if (filter.value.trim()) return;368 if (folder.open) opened.add(folder.dataset.path);369 else opened.delete(folder.dataset.path);370 remember(key, [...opened]);371 });372 }373 let selected = null;374 const show = (row) => {375 selected?.classList.remove("selected");376 selected = row;377 row.classList.add("selected");378 panel.replaceChildren(row.nextElementSibling.content.cloneNode(true));379 };380 for (const row of root.querySelectorAll(".atlas-row")) {381 row.addEventListener("pointerenter", (event) => {382 if (event.pointerType === "mouse") show(row);383 });384 row.addEventListener("focus", () => show(row));385 }386 const viewer = fileViewer(root);387 for (const file of files) {388 file.addEventListener("click", (event) => {389 if (modified(event)) return;390 event.preventDefault();391 viewer(file);392 });393 }394 const target = folders.find((folder) => folder.dataset.path === unfold)?.firstElementChild;395 if (target) {396 show(target);397 target.scrollIntoView({ block: "center" });398 }399 filter.addEventListener("input", () => {400 const query = filter.value.trim().toLowerCase();401 for (const file of files) file.hidden = query !== "" && !file.dataset.name.includes(query);402 for (const folder of folders) {403 folder.hidden = query !== "" && !folder.querySelector(".atlas-file:not([hidden])");404 folder.open = query !== "" || opened.has(folder.dataset.path);405 }406 });407 408 const search = document.querySelector(".atlas-search-btn");409 search.addEventListener("click", () => {410 search.hidden = true;411 filter.hidden = false;412 filter.focus();413 });414 filter.addEventListener("blur", () => {415 if (filter.value.trim() !== "") return;416 filter.hidden = true;417 search.hidden = false;418 });419 search.hidden = false;420 panel.hidden = false;421}422423424425function atlasPicker() {426 const picker = document.querySelector(".atlas-picker");427 if (!picker) return;428 const tabs = picker.querySelector(".atlas-picker-tabs");429 const filter = picker.querySelector(".atlas-picker-filter");430 const lists = [...picker.querySelectorAll(".atlas-picker-list")];431 let shown = lists[0];432 const narrow = () => {433 const query = filter.value.trim().toLowerCase();434 for (const row of shown.children) row.hidden = query !== "" && !row.dataset.q.includes(query);435 };436 for (const tab of tabs.children) {437 tab.addEventListener("click", () => {438 for (const each of tabs.children) each.classList.toggle("active", each === tab);439 shown = lists.find((list) => list.dataset.tab === tab.dataset.tab);440 for (const list of lists) list.hidden = list !== shown;441 filter.placeholder = `filter ${tab.dataset.tab}`;442 filter.value = "";443 narrow();444 filter.focus();445 });446 }447 filter.addEventListener("input", narrow);448 picker.addEventListener("toggle", () => {449 if (picker.open) filter.focus();450 });451 document.addEventListener("click", (event) => {452 if (!picker.contains(event.target)) picker.open = false;453 });454 picker.addEventListener("keydown", (event) => {455 if (event.key === "Escape") picker.open = false;456 });457 tabs.hidden = false;458 filter.hidden = false;459}460461462463464function fileViewer(root) {465 let [row, size, wrap] = [null, 12, false];466 const overlay = element("div", "fv-overlay");467 overlay.tabIndex = -1;468 overlay.setAttribute("role", "dialog");469 overlay.setAttribute("aria-modal", "true");470 const close = () => {471 overlay.remove();472 row?.focus();473 row = null;474 };475 const step = (by) => {476 const rows = [...root.querySelectorAll(".atlas-file")].filter((file) => !file.hidden);477 const next = rows[rows.indexOf(row) + by];478 if (next) open(next);479 };480 const restyle = () => {481 overlay.style.setProperty("--fv-size", `${size}px`);482 overlay.classList.toggle("fv-wrap", wrap);483 };484 const open = async (file, url = file.href) => {485 row = file;486 const rows = [...root.querySelectorAll(".atlas-file")].filter((each) => !each.hidden);487 const index = rows.indexOf(file);488 const detail = file.nextElementSibling.content.cloneNode(true);489 const name = detail.querySelector(".atlas-info-name").textContent;490 const path = detail.querySelector(".atlas-info-path").textContent;491 const raw = new URL(file.href);492 raw.pathname = raw.pathname.replace(/^(\/[^/]+)\/files\//, "$1/raw/");493 const button = (text, action, className = "fv-ctrl-btn") => {494 const b = element("button", className, text);495 b.type = "button";496 b.addEventListener("click", action);497 return b;498 };499 const link = (text, download) => {500 const a = element("a", "fv-ctrl-btn", text);501 a.href = raw.href;502 if (download) a.download = name;503 return a;504 };505 const source = element("div", "fv-source", "loading…");506 const panel = element("aside", "fv-detail");507 panel.append(detail);508 const header = element("div", "fv-header");509 const title = element("div", "fv-title");510 title.append(element("span", "fv-name", name), element("span", "fv-path mono", path));511 const controls = element("div", "fv-controls mono");512 controls.append(513 button("A−", () => {514 size = Math.max(9, size - 1);515 restyle();516 }),517 button("A+", () => {518 size = Math.min(20, size + 1);519 restyle();520 }),521 button("⇌ wrap", (event) => {522 wrap = !wrap;523 event.currentTarget.classList.toggle("active", wrap);524 restyle();525 }),526 link("raw", false),527 link("download", true),528 );529 controls.children[2].classList.toggle("active", wrap);530 const nav = element("div", "fv-nav mono");531 const back = button("←", () => step(-1), "fv-ctrl-btn");532 const forward = button("→", () => step(1), "fv-ctrl-btn");533 back.disabled = index <= 0;534 forward.disabled = index >= rows.length - 1;535 nav.append(back, element("span", "fv-pos", `${index + 1}/${rows.length}`), forward);536 header.append(button("×", close, "fv-close"), title, controls, nav);537 const body = element("div", "fv-body");538 body.append(source, panel);539 overlay.replaceChildren(header, body);540 restyle();541 if (!overlay.isConnected) document.body.append(overlay);542 overlay.focus();543 const main = await fetchMain(url);544 if (row !== file) return;545 if (!main) {546 location.href = url;547 return;548 }549 for (const heading of main.querySelectorAll(".breadcrumb, .file-page-title")) heading.remove();550 main.querySelector(".file-meta:not(:last-child)")?.remove();551 source.replaceChildren(...main.childNodes);552 };553 overlay.addEventListener("keydown", (event) => {554 if (event.key === "Escape") close();555 else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {556 event.preventDefault();557 step(event.key === "ArrowLeft" ? -1 : 1);558 }559 });560 561 overlay.addEventListener("click", (event) => {562 const link = event.target.closest(".fv-source a[href]");563 if (!link || !row || modified(event)) return;564 if (new URL(link.href).pathname !== new URL(row.href).pathname) return;565 event.preventDefault();566 open(row, link.href);567 });568 return open;569}570571function element(tag, className, text) {572 const node = document.createElement(tag);573 node.className = className;574 if (text !== undefined) node.textContent = text;575 return node;576}577578579580function remembered(key) {581 try {582 const value = JSON.parse(localStorage.getItem(key) ?? "[]");583 return Array.isArray(value) && value.every((path) => typeof path === "string") ? value : [];584 } catch {585 return [];586 }587}588589function remember(key, value) {590 try {591 localStorage.setItem(key, JSON.stringify(value));592 } catch {}593}594595596597function hexGlow() {598 const columns = document.querySelectorAll(".hex-col");599 if (columns.length === 0) return;600 const reduce = matchMedia("(prefers-reduced-motion: reduce)");601 let timer = null;602 const update = () => {603 clearInterval(timer);604 for (const column of columns) column.classList.remove("glow");605 if (reduce.matches) return;606 timer = setInterval(() => {607 const column = columns[Math.floor(Math.random() * columns.length)];608 if (document.querySelectorAll(".hex-col.glow").length >= 3) return;609 column.classList.add("glow");610 setTimeout(() => column.classList.remove("glow"), 3200);611 }, 1500);612 };613 reduce.addEventListener("change", update);614 update();615}616617618619function tickerPace() {620 for (const track of document.querySelectorAll(".ticker-track")) {621 const speed = Number(getComputedStyle(track).getPropertyValue("--speed"));622 if (speed > 0) track.style.animationDuration = `${track.scrollWidth / 2 / speed}s`;623 }624}625626627document.fonts.ready.then(tickerPace);628timeline();629copyButtons(document);630panels();631saveLists();632atlasPicker();633lineRanges();634atlas();635hexGlow();