520bd4107571
hide .kohignore from the Atlas, let the Atlas shrink below 520px, and keep issue pages and their forms inside the inline panel (v3.0.101)
- time
- 2026-09-29 18:06 UTC · 2 weeks ago
- machine
- Junes-MacBook-Pro.local
- agent
- claude-opus-5-5
- session
- match kepr 2 repo page
- parent
- 8ac8cee09624
↓ kepr-520bd410.tar.gzatlas at this save
files changed5 files
package.json+1 −1public/css/kepr.css+2 −2public/js/kepr.js+36 −16src/pages/atlas.ts+2 −1tests/pages.test.ts+0 −1package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.100",3+ "version": "3.0.101",44 "private": true,55 "type": "module",66 "scripts": {public/css/kepr.css+2 −2
@@ -679,12 +679,12 @@679679 .atlas-filter::placeholder {680680 color: var(--text-faint);681681 }682-/* Kepr 2's Atlas height: the tree and the panel each scroll inside it. */682+/* At most Kepr 2's Atlas height, the tree and the panel each scrolling inside it. */683683 .atlas {684684 display: grid;685685 grid-template-columns: minmax(0, 1fr);686686 gap: 0 20px;687- height: 520px;687+ max-height: 520px;688688 border-top: 1px solid var(--bg-elevated);689689 border-bottom: 1px solid var(--bg-elevated);690690 }public/js/kepr.js+36 −16
@@ -225,16 +225,16 @@225225 }226226 }227227 228-// A Kepr page's <main>, its links made absolute so they point where they did on their own page;229-// null if it could not be had, and the caller then goes to the page itself.230-async function fetchMain(url) {228+// A Kepr page's <main>, its links made absolute so they point where they did on their own page229+// (after any redirect). An error page's <main> comes back too, so a refused form shows why; null if230+// nothing came back, and the caller then goes to the page itself.231+async function fetchMain(url, init) {231232 try {232- const response = await fetch(url);233- if (!response.ok) return null;233+ const response = await fetch(url, init);234234 const page = new DOMParser().parseFromString(await response.text(), "text/html");235235 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;236+ for (const link of main?.querySelectorAll("a[href]:not([href^='#'])") ?? []) {237+ link.href = new URL(link.getAttribute("href"), response.url || url).href;238238 }239239 return main;240240 } catch {@@ -248,16 +248,18 @@248248 }249249 250250 // §21.9's panels open inline under the tabs, as Kepr 2's did: the panel page without its heading.251-// A link back to the same page (the issue tabs) loads in place; any other link navigates.251+// The panel's own pages (an issue, the report form) and its forms stay inside it; a link elsewhere,252+// or to a file (a download), navigates. The open tab takes the panel back to its first page, and253+// closes it from there.252254 function panels() {253255 const host = document.querySelector(".repo-panel");254256 if (!host) return;255257 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;258+ let shown = "";259+ const load = async (url, init) => {260+ shown = url;261+ const main = await fetchMain(url, init);262+ if (shown !== url) return;261263 if (!main) {262264 location.href = url;263265 return;@@ -268,14 +270,23 @@268270 copyButtons(host);269271 host.hidden = false;270272 };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+ };271278 for (const tab of document.querySelectorAll(".repo-tab[data-panel]")) {272279 tab.addEventListener("click", (event) => {273280 if (modified(event)) return;274281 event.preventDefault();282+ if (open === tab && shown !== tab.href) {283+ load(tab.href);284+ return;285+ }275286 open?.classList.remove("active");276287 if (open === tab) {277288 open = null;278- wanted = "";289+ shown = "";279290 host.hidden = true;280291 return;281292 }@@ -286,10 +297,19 @@286297 }287298 host.addEventListener("click", (event) => {288299 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;300+ if (!link || !open || modified(event) || link.hasAttribute("download")) return;301+ if (!inside(link.href)) return;291302 event.preventDefault();292303 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 });293313 });294314 }295315 src/pages/atlas.ts+2 −1
@@ -70,7 +70,8 @@7070 const touches = fileTouches(repo.mirror, saveId);7171 const foldersTouches = folderTouches(touches);7272 const root: AtlasFolder = { name: "", path: "", folders: [], files: [], saves: [] };73- const entries = filesAt(repo.mirror, saveId);73+ // Koh's own rules file is bookkeeping, not part of the project; Koh never saves what it names.74+ const entries = filesAt(repo.mirror, saveId).filter((entry) => entry.path !== ".kohignore");7475 for (const entry of entries) {7576 const parts = entry.path.split("/");7677 let folder = root;tests/pages.test.ts+0 −1
@@ -251,7 +251,6 @@251251 "main.ts",252252 "vendor/",253253 "big.c",254- ".kohignore",255254 "README.md",256255 "secret.txt",257256 ]);