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 63 height = 64;64 canvas.width = Math.round(width * devicePixelRatio);65 canvas.height = Math.round(height * devicePixelRatio);66 canvas.style.width = `${width}px`;67 canvas.style.height = `${height}px`;68 ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);69 strip = Math.max(width * 3, saves.length * 48);70 radius = 4;71 let previous = Number.NEGATIVE_INFINITY;72 for (const save of saves) {73 save.x = Math.max(xOf(save.t), previous + radius + 8);74 previous = save.x;75 }76 offset = Math.max(0, previous - width * 0.82);77 };78 79 const onScreen = (x, margin) => {80 const shift = ((offset % strip) + strip) % strip;81 return [-1, 0, 1]82 .map((k) => x - shift + k * strip)83 .filter((sx) => sx > -margin && sx < width + margin);84 };85 const hit = (event) => {86 const box = canvas.getBoundingClientRect();87 const [mx, my] = [event.clientX - box.left, event.clientY - box.top];88 if (Math.abs(my - height / 2) > radius + 7) return null;89 return (90 saves.find((save) => onScreen(save.x, 0).some((x) => Math.abs(mx - x) < radius + 7)) ?? null91 );92 };93 let [hovered, drag, dragFrom, downX, lastX, velocity, resumeAt, lastFrame] = [94 null,95 null,96 0,97 0,98 0,99 0,100 0,101 0,102 ];103 const hover = (save, event) => {104 if (save !== hovered) {105 hovered = save;106 tip.hidden = save === null;107 if (save)108 tip.replaceChildren(109 line("tip-id", save.id),110 line("tip-msg", save.message),111 line("tip-meta", save.meta),112 );113 }114 if (save) {115 const left = event.clientX + 14;116 tip.style.left = `${left + 340 > innerWidth ? Math.max(8, event.clientX - 354) : left}px`;117 tip.style.top = `${event.clientY - 10}px`;118 }119 canvas.style.cursor = save ? "pointer" : drag === null ? "grab" : "grabbing";120 };121 const draw = (time) => {122 const dt = lastFrame ? Math.min((time - lastFrame) / 1000, 0.1) : 0;123 lastFrame = time;124 if (drag === null && hovered === null) {125 if (Math.abs(velocity) > 0.5) {126 offset -= velocity;127 velocity *= 0.92;128 } else if (!reduce.matches && time >= resumeAt) offset += 20 * dt;129 }130 const y = height / 2;131 ctx.clearRect(0, 0, width, height);132 ctx.lineWidth = 1;133 ctx.strokeStyle = css.getPropertyValue("--border");134 const years = [new Date(start * 1000).getFullYear(), new Date(end * 1000).getFullYear()];135 for (let year = years[0]; year <= years[1]; year++) {136 for (const x of onScreen(xOf(new Date(year, 0, 1) / 1000), 0)) {137 ctx.beginPath();138 ctx.moveTo(x, height * 0.1);139 ctx.lineTo(x, height * 0.9);140 ctx.stroke();141 }142 }143 ctx.globalAlpha = 0.7;144 ctx.strokeStyle = css.getPropertyValue("--text-muted");145 ctx.setLineDash([4, 4]);146 ctx.beginPath();147 ctx.moveTo(0, y);148 ctx.lineTo(width, y);149 ctx.stroke();150 ctx.setLineDash([]);151 ctx.font = '10px "Martian Mono", monospace';152 ctx.textAlign = "center";153 for (const save of saves) {154 for (const x of onScreen(save.x, 10)) {155 ctx.globalAlpha = hovered === null ? save.alpha : hovered === save ? 1 : save.alpha * 0.6;156 ctx.fillStyle = save.fill;157 ctx.beginPath();158 ctx.arc(x, y, radius, 0, Math.PI * 2);159 ctx.fill();160 if (hovered === save) {161 ctx.fillStyle = css.getPropertyValue("--accent");162 ctx.fillText(save.id.slice(0, 6), x, y - radius - 6);163 }164 }165 }166 ctx.globalAlpha = 1;167 requestAnimationFrame(draw);168 };169 canvas.addEventListener("pointerdown", (event) => {170 [drag, dragFrom, downX, lastX, velocity] = [171 event.clientX,172 offset,173 event.clientX,174 event.clientX,175 0,176 ];177 canvas.setPointerCapture(event.pointerId);178 });179 canvas.addEventListener("pointermove", (event) => {180 if (drag !== null) {181 offset = dragFrom - (event.clientX - drag);182 velocity = velocity * 0.6 + (event.clientX - lastX) * 0.4;183 lastX = event.clientX;184 }185 if (event.pointerType === "mouse") hover(hit(event), event);186 });187 const release = () => {188 drag = null;189 resumeAt = performance.now() + 900;190 };191 canvas.addEventListener("pointerup", (event) => {192 release();193 const save = Math.abs(event.clientX - downX) > 4 ? null : hit(event);194 if (save) location.href = save.href;195 });196 canvas.addEventListener("pointercancel", release);197 canvas.addEventListener("pointerleave", (event) => {198 if (drag === null) hover(null, event);199 });200 addEventListener("resize", resize);201 resize();202 requestAnimationFrame(draw);203}204205function line(className, text) {206 const div = document.createElement("div");207 div.className = className;208 div.textContent = text ?? "";209 return div;210}211212function copyButtons(root) {213 for (const button of root.querySelectorAll("button[data-copy]")) {214 const label = button.textContent;215 button.hidden = false;216 button.addEventListener("click", async () => {217 await navigator.clipboard.writeText(button.dataset.copy);218 button.textContent = "copied";219 button.classList.add("copied");220 setTimeout(() => {221 button.textContent = label;222 button.classList.remove("copied");223 }, 1400);224 });225 }226}227228229230231async function fetchMain(url, init) {232 try {233 const response = await fetch(url, init);234 const page = new DOMParser().parseFromString(await response.text(), "text/html");235 const main = page.querySelector("main");236 for (const link of main?.querySelectorAll("a[href]:not([href^='#'])") ?? []) {237 link.href = new URL(link.getAttribute("href"), response.url || url).href;238 }239 return main;240 } catch {241 return null;242 }243}244245246function modified(event) {247 return event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;248}249250251252253254function panels() {255 const host = document.querySelector(".repo-panel");256 if (!host) return;257 let open = null;258 let shown = "";259 const load = async (url, init) => {260 shown = url;261 const main = await fetchMain(url, init);262 if (shown !== url) return;263 if (!main) {264 location.href = url;265 return;266 }267 main.querySelector(".breadcrumb")?.remove();268 main.querySelector(".file-page-title")?.remove();269 host.replaceChildren(...main.childNodes);270 copyButtons(host);271 host.hidden = false;272 };273 const inside = (url) => {274 const base = new URL(open.href).pathname;275 const { pathname } = new URL(url);276 return (pathname === base || pathname.startsWith(`${base}/`)) && !/\.[^/]*$/.test(pathname);277 };278 for (const tab of document.querySelectorAll(".repo-tab[data-panel]")) {279 tab.addEventListener("click", (event) => {280 if (modified(event)) return;281 event.preventDefault();282 if (open === tab && shown !== tab.href) {283 load(tab.href);284 return;285 }286 open?.classList.remove("active");287 if (open === tab) {288 open = null;289 shown = "";290 host.hidden = true;291 return;292 }293 open = tab;294 tab.classList.add("active");295 load(tab.href);296 });297 }298 host.addEventListener("click", (event) => {299 const link = event.target.closest("a[href]");300 if (!link || !open || modified(event) || link.hasAttribute("download")) return;301 if (!inside(link.href)) return;302 event.preventDefault();303 load(link.href);304 });305 host.addEventListener("submit", (event) => {306 const form = event.target;307 if (!open || !inside(form.action)) return;308 event.preventDefault();309 const method = form.method.toUpperCase();310 const data = new FormData(form);311 if (method === "GET") load(`${form.action}?${new URLSearchParams(data)}`);312 else load(form.action, { method, body: data });313 });314}315316317318function saveLists() {319 for (const list of document.querySelectorAll(".save-list")) {320 const observer = new IntersectionObserver(321 async ([entry]) => {322 const older = list.querySelector(".pager-older");323 if (!entry?.isIntersecting || !older) return;324 observer.disconnect();325 const main = await fetchMain(older.href);326 const next = main?.querySelector(`.save-list[data-list="${list.dataset.list}"]`);327 if (!next) return;328 next.querySelector(".pager a:not(.pager-older)")?.remove();329 list.querySelector(".pager").replaceWith(...next.childNodes);330 watch();331 },332 { root: list },333 );334 const watch = () => {335 const older = list.querySelector(".pager-older");336 if (older) observer.observe(older);337 };338 watch();339 }340}341342343function lineRanges() {344 const code = document.querySelector(".code");345 if (!code) return;346 let anchor = null;347 const mark = () => {348 for (const marked of code.querySelectorAll(".line.hl")) marked.classList.remove("hl");349 const match = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash);350 if (!match) return;351 const from = Number(match[1]);352 const to = Number(match[2] ?? match[1]);353 anchor = from;354 for (let n = Math.min(from, to); n <= Math.max(from, to); n++) {355 document.getElementById(`L${n}`)?.classList.add("hl");356 }357 document.getElementById(`L${Math.min(from, to)}`)?.scrollIntoView({ block: "center" });358 };359 code.addEventListener("click", (event) => {360 const link = event.target.closest(".ln");361 if (!link || !event.shiftKey || anchor === null) return;362 event.preventDefault();363 const n = Number(link.textContent);364 history.replaceState(null, "", `#L${Math.min(anchor, n)}-L${Math.max(anchor, n)}`);365 mark();366 });367 window.addEventListener("hashchange", mark);368 mark();369}370371372373function atlas() {374 const root = document.querySelector(".atlas");375 const filter = document.querySelector(".atlas-filter");376 if (!root || !filter) return;377 const panel = root.querySelector(".atlas-panel");378 const folders = [...root.querySelectorAll(".atlas-folder")];379 const files = [...root.querySelectorAll(".atlas-file")];380 const key = `kepr:atlas:${root.dataset.repo}`;381 const opened = new Set(remembered(key));382 383 const unfold = new URLSearchParams(location.search).get("open");384 const onPath = (path) => unfold !== null && `${unfold}/`.startsWith(`${path}/`);385 for (const folder of folders) {386 folder.open = opened.has(folder.dataset.path) || onPath(folder.dataset.path);387 folder.addEventListener("toggle", () => {388 if (filter.value.trim()) return;389 if (folder.open) opened.add(folder.dataset.path);390 else opened.delete(folder.dataset.path);391 remember(key, [...opened]);392 });393 }394 let selected = null;395 const show = (row) => {396 selected?.classList.remove("selected");397 selected = row;398 row.classList.add("selected");399 panel.replaceChildren(row.nextElementSibling.content.cloneNode(true));400 };401 for (const row of root.querySelectorAll(".atlas-row")) {402 row.addEventListener("pointerenter", (event) => {403 if (event.pointerType === "mouse") show(row);404 });405 row.addEventListener("focus", () => show(row));406 }407 const viewer = fileViewer(root);408 for (const file of files) {409 file.addEventListener("click", (event) => {410 if (modified(event)) return;411 event.preventDefault();412 viewer(file);413 });414 }415 const target = folders.find((folder) => folder.dataset.path === unfold)?.firstElementChild;416 if (target) {417 show(target);418 target.scrollIntoView({ block: "center" });419 }420 filter.addEventListener("input", () => {421 const query = filter.value.trim().toLowerCase();422 for (const file of files) file.hidden = query !== "" && !file.dataset.name.includes(query);423 for (const folder of folders) {424 folder.hidden = query !== "" && !folder.querySelector(".atlas-file:not([hidden])");425 folder.open = query !== "" || opened.has(folder.dataset.path);426 }427 });428 429 const search = document.querySelector(".atlas-search-btn");430 search.addEventListener("click", () => {431 search.hidden = true;432 filter.hidden = false;433 filter.focus();434 });435 filter.addEventListener("blur", () => {436 if (filter.value.trim() !== "") return;437 filter.hidden = true;438 search.hidden = false;439 });440 search.hidden = false;441 panel.hidden = false;442}443444445function atlasPicker() {446 const picker = document.querySelector(".atlas-picker");447 if (!picker) return;448 const filter = picker.querySelector(".atlas-picker-filter");449 const rows = [...picker.querySelectorAll(".atlas-picker-row")];450 filter.addEventListener("input", () => {451 const query = filter.value.trim().toLowerCase();452 for (const row of rows) row.hidden = query !== "" && !row.dataset.q.includes(query);453 });454 picker.addEventListener("toggle", () => {455 if (picker.open) filter.focus();456 });457 document.addEventListener("click", (event) => {458 if (!picker.contains(event.target)) picker.open = false;459 });460 picker.addEventListener("keydown", (event) => {461 if (event.key === "Escape") picker.open = false;462 });463 filter.hidden = false;464}465466467468469function fileViewer(root) {470 let [row, size, wrap] = [null, 12, false];471 const overlay = element("div", "fv-overlay");472 overlay.tabIndex = -1;473 overlay.setAttribute("role", "dialog");474 overlay.setAttribute("aria-modal", "true");475 const close = () => {476 overlay.remove();477 row?.focus();478 row = null;479 };480 const step = (by) => {481 const rows = [...root.querySelectorAll(".atlas-file")].filter((file) => !file.hidden);482 const next = rows[rows.indexOf(row) + by];483 if (next) open(next);484 };485 const restyle = () => {486 overlay.style.setProperty("--fv-size", `${size}px`);487 overlay.classList.toggle("fv-wrap", wrap);488 };489 const open = async (file, url = file.href) => {490 row = file;491 const rows = [...root.querySelectorAll(".atlas-file")].filter((each) => !each.hidden);492 const index = rows.indexOf(file);493 const detail = file.nextElementSibling.content.cloneNode(true);494 const name = detail.querySelector(".atlas-info-name").textContent;495 const path = detail.querySelector(".atlas-info-path").textContent;496 const raw = new URL(file.href);497 raw.pathname = raw.pathname.replace(/^(\/[^/]+)\/files\//, "$1/raw/");498 const button = (text, action, className = "fv-ctrl-btn") => {499 const b = element("button", className, text);500 b.type = "button";501 b.addEventListener("click", action);502 return b;503 };504 const link = (text, download) => {505 const a = element("a", "fv-ctrl-btn", text);506 a.href = raw.href;507 if (download) a.download = name;508 return a;509 };510 const source = element("div", "fv-source", "loading…");511 const panel = element("aside", "fv-detail");512 panel.append(detail);513 const header = element("div", "fv-header");514 const title = element("div", "fv-title");515 title.append(element("span", "fv-name", name), element("span", "fv-path mono", path));516 const controls = element("div", "fv-controls mono");517 controls.append(518 button("A−", () => {519 size = Math.max(9, size - 1);520 restyle();521 }),522 button("A+", () => {523 size = Math.min(20, size + 1);524 restyle();525 }),526 button("⇌ wrap", (event) => {527 wrap = !wrap;528 event.currentTarget.classList.toggle("active", wrap);529 restyle();530 }),531 link("raw", false),532 link("download", true),533 );534 controls.children[2].classList.toggle("active", wrap);535 const nav = element("div", "fv-nav mono");536 const back = button("←", () => step(-1), "fv-ctrl-btn");537 const forward = button("→", () => step(1), "fv-ctrl-btn");538 back.disabled = index <= 0;539 forward.disabled = index >= rows.length - 1;540 nav.append(back, element("span", "fv-pos", `${index + 1}/${rows.length}`), forward);541 header.append(button("×", close, "fv-close"), title, controls, nav);542 const body = element("div", "fv-body");543 body.append(source, panel);544 overlay.replaceChildren(header, body);545 restyle();546 if (!overlay.isConnected) document.body.append(overlay);547 overlay.focus();548 const main = await fetchMain(url);549 if (row !== file) return;550 if (!main) {551 location.href = url;552 return;553 }554 for (const heading of main.querySelectorAll(".breadcrumb, .file-page-title")) heading.remove();555 main.querySelector(".file-meta:not(:last-child)")?.remove();556 source.replaceChildren(...main.childNodes);557 };558 overlay.addEventListener("keydown", (event) => {559 if (event.key === "Escape") close();560 else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {561 event.preventDefault();562 step(event.key === "ArrowLeft" ? -1 : 1);563 }564 });565 566 overlay.addEventListener("click", (event) => {567 const link = event.target.closest(".fv-source a[href]");568 if (!link || !row || modified(event)) return;569 if (new URL(link.href).pathname !== new URL(row.href).pathname) return;570 event.preventDefault();571 open(row, link.href);572 });573 return open;574}575576function element(tag, className, text) {577 const node = document.createElement(tag);578 node.className = className;579 if (text !== undefined) node.textContent = text;580 return node;581}582583584585function remembered(key) {586 try {587 const value = JSON.parse(localStorage.getItem(key) ?? "[]");588 return Array.isArray(value) && value.every((path) => typeof path === "string") ? value : [];589 } catch {590 return [];591 }592}593594function remember(key, value) {595 try {596 localStorage.setItem(key, JSON.stringify(value));597 } catch {}598}599600601602function hexGlow() {603 const columns = document.querySelectorAll(".hex-col");604 if (columns.length === 0) return;605 const reduce = matchMedia("(prefers-reduced-motion: reduce)");606 let timer = null;607 const update = () => {608 clearInterval(timer);609 for (const column of columns) column.classList.remove("glow");610 if (reduce.matches) return;611 timer = setInterval(() => {612 const column = columns[Math.floor(Math.random() * columns.length)];613 if (document.querySelectorAll(".hex-col.glow").length >= 3) return;614 column.classList.add("glow");615 setTimeout(() => column.classList.remove("glow"), 3200);616 }, 1500);617 };618 reduce.addEventListener("change", update);619 update();620}621622623624function tickerPace() {625 for (const track of document.querySelectorAll(".ticker-track")) {626 const speed = Number(getComputedStyle(track).getPropertyValue("--speed"));627 if (speed > 0) track.style.animationDuration = `${track.scrollWidth / 2 / speed}s`;628 }629}630631632document.fonts.ready.then(tickerPace);633timeline();634copyButtons(document);635panels();636saveLists();637atlasPicker();638lineRanges();639atlas();640hexGlow();