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
5 files package.json+1 −1public/css/kepr.css+2 −2public/js/kepr.js+36 −16src/pages/atlas.ts+2 −1tests/pages.test.ts+0 −1
package.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": {

view file

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 }

view file

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 

view file

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;

view file

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 		]);

view file