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}227228229230async function fetchMain(url) {231 try {232 const response = await fetch(url);233 if (!response.ok) return null;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"), 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}249250251252function panels() {253 const host = document.querySelector(".repo-panel");254 if (!host) return;255 let open = null;256 let wanted = "";257 const load = async (url) => {258 wanted = url;259 const main = await fetchMain(url);260 if (wanted !== url) return;261 if (!main) {262 location.href = url;263 return;264 }265 main.querySelector(".breadcrumb")?.remove();266 main.querySelector(".file-page-title")?.remove();267 host.replaceChildren(...main.childNodes);268 copyButtons(host);269 host.hidden = false;270 };271 for (const tab of document.querySelectorAll(".repo-tab[data-panel]")) {272 tab.addEventListener("click", (event) => {273 if (modified(event)) return;274 event.preventDefault();275 open?.classList.remove("active");276 if (open === tab) {277 open = null;278 wanted = "";279 host.hidden = true;280 return;281 }282 open = tab;283 tab.classList.add("active");284 load(tab.href);285 });286 }287 host.addEventListener("click", (event) => {288 const link = event.target.closest("a[href]");289 if (!link || !open || modified(event)) return;290 if (new URL(link.href).pathname !== new URL(open.href).pathname) return;291 event.preventDefault();292 load(link.href);293 });294}295296297298function saveLists() {299 for (const list of document.querySelectorAll(".save-list")) {300 const observer = new IntersectionObserver(301 async ([entry]) => {302 const older = list.querySelector(".pager-older");303 if (!entry?.isIntersecting || !older) return;304 observer.disconnect();305 const main = await fetchMain(older.href);306 const next = main?.querySelector(`.save-list[data-list="${list.dataset.list}"]`);307 if (!next) return;308 next.querySelector(".pager a:not(.pager-older)")?.remove();309 list.querySelector(".pager").replaceWith(...next.childNodes);310 watch();311 },312 { root: list },313 );314 const watch = () => {315 const older = list.querySelector(".pager-older");316 if (older) observer.observe(older);317 };318 watch();319 }320}321322323function lineRanges() {324 const code = document.querySelector(".code");325 if (!code) return;326 let anchor = null;327 const mark = () => {328 for (const marked of code.querySelectorAll(".line.hl")) marked.classList.remove("hl");329 const match = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash);330 if (!match) return;331 const from = Number(match[1]);332 const to = Number(match[2] ?? match[1]);333 anchor = from;334 for (let n = Math.min(from, to); n <= Math.max(from, to); n++) {335 document.getElementById(`L${n}`)?.classList.add("hl");336 }337 document.getElementById(`L${Math.min(from, to)}`)?.scrollIntoView({ block: "center" });338 };339 code.addEventListener("click", (event) => {340 const link = event.target.closest(".ln");341 if (!link || !event.shiftKey || anchor === null) return;342 event.preventDefault();343 const n = Number(link.textContent);344 history.replaceState(null, "", `#L${Math.min(anchor, n)}-L${Math.max(anchor, n)}`);345 mark();346 });347 window.addEventListener("hashchange", mark);348 mark();349}350351352353function atlas() {354 const root = document.querySelector(".atlas");355 const filter = document.querySelector(".atlas-filter");356 if (!root || !filter) return;357 const panel = root.querySelector(".atlas-panel");358 const folders = [...root.querySelectorAll(".atlas-folder")];359 const files = [...root.querySelectorAll(".atlas-file")];360 const key = `kepr:atlas:${root.dataset.repo}`;361 const opened = new Set(remembered(key));362 363 const unfold = new URLSearchParams(location.search).get("open");364 const onPath = (path) => unfold !== null && `${unfold}/`.startsWith(`${path}/`);365 for (const folder of folders) {366 folder.open = opened.has(folder.dataset.path) || onPath(folder.dataset.path);367 folder.addEventListener("toggle", () => {368 if (filter.value.trim()) return;369 if (folder.open) opened.add(folder.dataset.path);370 else opened.delete(folder.dataset.path);371 remember(key, [...opened]);372 });373 }374 let selected = null;375 const show = (row) => {376 selected?.classList.remove("selected");377 selected = row;378 row.classList.add("selected");379 panel.replaceChildren(row.nextElementSibling.content.cloneNode(true));380 };381 for (const row of root.querySelectorAll(".atlas-row")) {382 row.addEventListener("pointerenter", (event) => {383 if (event.pointerType === "mouse") show(row);384 });385 row.addEventListener("focus", () => show(row));386 }387 const viewer = fileViewer(root);388 for (const file of files) {389 file.addEventListener("click", (event) => {390 if (modified(event)) return;391 event.preventDefault();392 viewer(file);393 });394 }395 const target = folders.find((folder) => folder.dataset.path === unfold)?.firstElementChild;396 if (target) {397 show(target);398 target.scrollIntoView({ block: "center" });399 }400 filter.addEventListener("input", () => {401 const query = filter.value.trim().toLowerCase();402 for (const file of files) file.hidden = query !== "" && !file.dataset.name.includes(query);403 for (const folder of folders) {404 folder.hidden = query !== "" && !folder.querySelector(".atlas-file:not([hidden])");405 folder.open = query !== "" || opened.has(folder.dataset.path);406 }407 });408 409 const search = document.querySelector(".atlas-search-btn");410 search.addEventListener("click", () => {411 search.hidden = true;412 filter.hidden = false;413 filter.focus();414 });415 filter.addEventListener("blur", () => {416 if (filter.value.trim() !== "") return;417 filter.hidden = true;418 search.hidden = false;419 });420 search.hidden = false;421 panel.hidden = false;422}423424425function atlasPicker() {426 const picker = document.querySelector(".atlas-picker");427 if (!picker) return;428 const filter = picker.querySelector(".atlas-picker-filter");429 const rows = [...picker.querySelectorAll(".atlas-picker-row")];430 filter.addEventListener("input", () => {431 const query = filter.value.trim().toLowerCase();432 for (const row of rows) row.hidden = query !== "" && !row.dataset.q.includes(query);433 });434 picker.addEventListener("toggle", () => {435 if (picker.open) filter.focus();436 });437 document.addEventListener("click", (event) => {438 if (!picker.contains(event.target)) picker.open = false;439 });440 picker.addEventListener("keydown", (event) => {441 if (event.key === "Escape") picker.open = false;442 });443 filter.hidden = false;444}445446447448449function fileViewer(root) {450 let [row, size, wrap] = [null, 12, false];451 const overlay = element("div", "fv-overlay");452 overlay.tabIndex = -1;453 overlay.setAttribute("role", "dialog");454 overlay.setAttribute("aria-modal", "true");455 const close = () => {456 overlay.remove();457 row?.focus();458 row = null;459 };460 const step = (by) => {461 const rows = [...root.querySelectorAll(".atlas-file")].filter((file) => !file.hidden);462 const next = rows[rows.indexOf(row) + by];463 if (next) open(next);464 };465 const restyle = () => {466 overlay.style.setProperty("--fv-size", `${size}px`);467 overlay.classList.toggle("fv-wrap", wrap);468 };469 const open = async (file, url = file.href) => {470 row = file;471 const rows = [...root.querySelectorAll(".atlas-file")].filter((each) => !each.hidden);472 const index = rows.indexOf(file);473 const detail = file.nextElementSibling.content.cloneNode(true);474 const name = detail.querySelector(".atlas-info-name").textContent;475 const path = detail.querySelector(".atlas-info-path").textContent;476 const raw = new URL(file.href);477 raw.pathname = raw.pathname.replace(/^(\/[^/]+)\/files\//, "$1/raw/");478 const button = (text, action, className = "fv-ctrl-btn") => {479 const b = element("button", className, text);480 b.type = "button";481 b.addEventListener("click", action);482 return b;483 };484 const link = (text, download) => {485 const a = element("a", "fv-ctrl-btn", text);486 a.href = raw.href;487 if (download) a.download = name;488 return a;489 };490 const source = element("div", "fv-source", "loading…");491 const panel = element("aside", "fv-detail");492 panel.append(detail);493 const header = element("div", "fv-header");494 const title = element("div", "fv-title");495 title.append(element("span", "fv-name", name), element("span", "fv-path mono", path));496 const controls = element("div", "fv-controls mono");497 controls.append(498 button("A−", () => {499 size = Math.max(9, size - 1);500 restyle();501 }),502 button("A+", () => {503 size = Math.min(20, size + 1);504 restyle();505 }),506 button("⇌ wrap", (event) => {507 wrap = !wrap;508 event.currentTarget.classList.toggle("active", wrap);509 restyle();510 }),511 link("raw", false),512 link("download", true),513 );514 controls.children[2].classList.toggle("active", wrap);515 const nav = element("div", "fv-nav mono");516 const back = button("←", () => step(-1), "fv-ctrl-btn");517 const forward = button("→", () => step(1), "fv-ctrl-btn");518 back.disabled = index <= 0;519 forward.disabled = index >= rows.length - 1;520 nav.append(back, element("span", "fv-pos", `${index + 1}/${rows.length}`), forward);521 header.append(button("×", close, "fv-close"), title, controls, nav);522 const body = element("div", "fv-body");523 body.append(source, panel);524 overlay.replaceChildren(header, body);525 restyle();526 if (!overlay.isConnected) document.body.append(overlay);527 overlay.focus();528 const main = await fetchMain(url);529 if (row !== file) return;530 if (!main) {531 location.href = url;532 return;533 }534 for (const heading of main.querySelectorAll(".breadcrumb, .file-page-title")) heading.remove();535 main.querySelector(".file-meta:not(:last-child)")?.remove();536 source.replaceChildren(...main.childNodes);537 };538 overlay.addEventListener("keydown", (event) => {539 if (event.key === "Escape") close();540 else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {541 event.preventDefault();542 step(event.key === "ArrowLeft" ? -1 : 1);543 }544 });545 546 overlay.addEventListener("click", (event) => {547 const link = event.target.closest(".fv-source a[href]");548 if (!link || !row || modified(event)) return;549 if (new URL(link.href).pathname !== new URL(row.href).pathname) return;550 event.preventDefault();551 open(row, link.href);552 });553 return open;554}555556function element(tag, className, text) {557 const node = document.createElement(tag);558 node.className = className;559 if (text !== undefined) node.textContent = text;560 return node;561}562563564565function remembered(key) {566 try {567 const value = JSON.parse(localStorage.getItem(key) ?? "[]");568 return Array.isArray(value) && value.every((path) => typeof path === "string") ? value : [];569 } catch {570 return [];571 }572}573574function remember(key, value) {575 try {576 localStorage.setItem(key, JSON.stringify(value));577 } catch {}578}579580581582function hexGlow() {583 const columns = document.querySelectorAll(".hex-col");584 if (columns.length === 0) return;585 const reduce = matchMedia("(prefers-reduced-motion: reduce)");586 let timer = null;587 const update = () => {588 clearInterval(timer);589 for (const column of columns) column.classList.remove("glow");590 if (reduce.matches) return;591 timer = setInterval(() => {592 const column = columns[Math.floor(Math.random() * columns.length)];593 if (document.querySelectorAll(".hex-col.glow").length >= 3) return;594 column.classList.add("glow");595 setTimeout(() => column.classList.remove("glow"), 3200);596 }, 1500);597 };598 reduce.addEventListener("change", update);599 update();600}601602603604function tickerPace() {605 for (const track of document.querySelectorAll(".ticker-track")) {606 const speed = Number(getComputedStyle(track).getPropertyValue("--speed"));607 if (speed > 0) track.style.animationDuration = `${track.scrollWidth / 2 / speed}s`;608 }609}610611612document.fonts.ready.then(tickerPace);613timeline();614copyButtons(document);615panels();616saveLists();617atlasPicker();618lineRanges();619atlas();620hexGlow();