ffa764f54431
add the Atlas browser island: hover panel, filename filter and remembered open folders, with phase 2 recorded in continuity (v3.0.11)
- time
- 2026-09-26 18:13 UTC · 2 weeks ago
- machine
- Junes-MacBook-Pro.local
- agent
- claude-opus-5-5
- session
- kepr 3 implementation
- parent
- 5e4eb28dcc4a
↓ kepr-ffa764f5.tar.gzatlas at this save
files changed3 files
package.json+1 −1public/js/kepr.js+62 −1src/describe/describe.ts+2 −2package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.10",3+ "version": "3.0.11",44 "private": true,55 "type": "module",66 "scripts": {public/js/kepr.js+62 −1
@@ -1,5 +1,5 @@11 // Kepr's browser islands (§18). Every page is complete without this file; it only adds the2-// styled timeline tooltip, copy buttons and line-range highlighting.2+// styled timeline tooltip, copy buttons, line-range highlighting and the Atlas panel and filter.33 44 function timelineTips() {55 const dots = document.querySelectorAll(".timeline .dot");@@ -78,6 +78,67 @@7878 mark();7979 }8080 81+// The Atlas (§21.5): each row's detail panel waits in the <template> after it; the filter narrows82+// by filename; which folders are open is remembered per repo in this browser.83+function atlas() {84+ const root = document.querySelector(".atlas");85+ const filter = document.querySelector(".atlas-filter");86+ if (!root || !filter) return;87+ const panel = root.querySelector(".atlas-panel");88+ const folders = [...root.querySelectorAll(".atlas-folder")];89+ const files = [...root.querySelectorAll(".atlas-file")];90+ const key = `kepr:atlas:${root.dataset.repo}`;91+ const opened = new Set(remembered(key));92+ for (const folder of folders) {93+ folder.open = opened.has(folder.dataset.path);94+ folder.addEventListener("toggle", () => {95+ if (filter.value.trim()) return;96+ if (folder.open) opened.add(folder.dataset.path);97+ else opened.delete(folder.dataset.path);98+ remember(key, [...opened]);99+ });100+ }101+ let selected = null;102+ const show = (row) => {103+ selected?.classList.remove("selected");104+ selected = row;105+ row.classList.add("selected");106+ panel.replaceChildren(row.nextElementSibling.content.cloneNode(true));107+ };108+ for (const row of root.querySelectorAll(".atlas-row")) {109+ row.addEventListener("pointerenter", (event) => {110+ if (event.pointerType === "mouse") show(row);111+ });112+ row.addEventListener("focus", () => show(row));113+ }114+ filter.addEventListener("input", () => {115+ const query = filter.value.trim().toLowerCase();116+ for (const file of files) file.hidden = query !== "" && !file.dataset.name.includes(query);117+ for (const folder of folders) {118+ folder.hidden = query !== "" && !folder.querySelector(".atlas-file:not([hidden])");119+ folder.open = query !== "" || opened.has(folder.dataset.path);120+ }121+ });122+ filter.hidden = false;123+ panel.hidden = false;124+}125+126+// Storage can be missing or refused (private windows, blocked site data); the tree then starts closed.127+function remembered(key) {128+ try {129+ return JSON.parse(localStorage.getItem(key) ?? "[]");130+ } catch {131+ return [];132+ }133+}134+135+function remember(key, value) {136+ try {137+ localStorage.setItem(key, JSON.stringify(value));138+ } catch {}139+}140+81141 timelineTips();82142 copyButtons();83143 lineRanges();144+atlas();src/describe/describe.ts+2 −2
@@ -1,7 +1,7 @@1-import MarkdownIt from "markdown-it";2-31 // §21.5: a description is the file's own words, found by syntax alone; no comment means none.42 // Every function returns Markdown source (hard-wrapped lines kept) or null.3+4+import MarkdownIt from "markdown-it";55 66 type Style = "bang" | "slash" | "jvm" | "hash" | "python" | "markdown";77