7531ccf13b73

make repo selection vertical and expose global hide (v3.0.206)

time
2026-10-03 14:38 UTC · 1 week ago
machine
Junes-MacBook-Pro.local
agent
gpt-6.1-sol
session
tidy operator navigation
parent
2b85afaa7b7f
5 files package.json+1 −1public/css/kepr.css+50 −9src/admin/repos.ts+13 −8src/admin/site.ts+13 −6tests/ignored.test.ts+11 −0
package.json+1 −1
@@ -1,6 +1,6 @@11 {22 	"name": "kepr",3-	"version": "3.0.205",3+	"version": "3.0.206",44 	"private": true,55 	"type": "module",66 	"scripts": {

view file

public/css/kepr.css+50 −9
@@ -2333,17 +2333,58 @@23332333 .admin-releases li {23342334 	padding: 4px 0;23352335 }2336-.admin-repos-bar {2336+.admin-repos-layout {2337+	display: grid;2338+	grid-template-columns: 140px minmax(0, 1fr);2339+	gap: 24px;2340+}2341+.admin-repo-picker {2342+	position: sticky;2343+	top: 24px;2344+	align-self: start;23372345 	display: flex;2338-	flex-wrap: wrap;2339-	align-items: flex-start;2340-	justify-content: space-between;2341-	gap: 12px;2342-	margin-bottom: 16px;2346+	flex-direction: column;2347+	gap: 20px;2348+	font-size: 0.75rem;23432349 }2344-.admin-repos-bar .issues-bar {2345-	flex: 1 1 auto;2346-	padding-bottom: 0;2350+.admin-repo-picker nav {2351+	display: flex;2352+	flex-direction: column;2353+	max-height: 50vh;2354+	overflow-y: auto;2355+	border-bottom: 1px solid var(--border);2356+}2357+.admin-repo-link {2358+	padding: 12px 8px;2359+	overflow-wrap: anywhere;2360+	border-left: 2px solid transparent;2361+	color: var(--text-secondary);2362+}2363+.admin-repo-link.active {2364+	border-left-color: var(--accent);2365+	background: var(--bg-surface);2366+	color: var(--accent-bright);2367+}2368+.admin-repo-link span {2369+	display: block;2370+	font-size: 0.625rem;2371+}2372+.admin-repo-workspace {2373+	min-width: 0;2374+}2375+#global-hide {2376+	scroll-margin-top: 24px;2377+}2378+@media (max-width: 900px) {2379+	.admin-repos-layout {2380+		grid-template-columns: minmax(0, 1fr);2381+	}2382+	.admin-repo-picker {2383+		position: static;2384+	}2385+	.admin-repo-picker nav {2386+		max-height: 200px;2387+	}23472388 }23482389 @media (max-width: 680px) {23492390 	.admin-layout {

view file

src/admin/repos.ts+13 −8
@@ -209,23 +209,27 @@209209 function reposPage(c: Context, ctx: AdminContext, error: string, chosen: string | undefined) {210210 	const repos = allRepos(ctx.kepr);211211 	const selected = repos.find((repo) => repo.name === chosen) ?? repos[0];212-	// One click per repo (June, 2026-10-02): the repos are the tabs, and each holds all of its own.213-	const tab = (repo: AdminRepo) => {212+	// Each repo keeps its whole workspace behind one link.213+	const repoLink = (repo: AdminRepo) => {214214 		const current = repo.name === selected?.name;215-		return html`<a class="issues-tab${current ? " active" : ""}" href="/admin/repos?repo=${repo.name}"${215+		return html`<a class="admin-repo-link${current ? " active" : ""}" href="/admin/repos?repo=${repo.name}"${216216 			current ? html` aria-current="page"` : ""217217 		}>${repo.name}${repo.public ? "" : html` <span class="muted">private</span>`}${218218 			repo.last_sync_error ? html` <span class="issues-error">sync failed</span>` : ""219219 		}</a>`;220220 	};221221 	const body = html`${notice(error)}222-<div class="admin-repos-bar">223-<nav class="issues-bar mono" aria-label="repositories">${repos.map(tab)}</nav>222+<div class="admin-repos-layout">223+<aside class="admin-repo-picker mono">224+<nav aria-label="repositories">${repos.map(repoLink)}</nav>225+<a href="/admin/site#global-hide">global hide ↗</a>224226 <form method="post" action="/admin/sync">225227 ${formField(c)}<button class="issues-submit" type="submit">sync now</button></form>226-</div>228+</aside>229+<div class="admin-repo-workspace">227230 ${repos.length === 0 && html`<p class="muted mono save-empty">no repos found in the bucket yet</p>`}228-${selected && repoWorkspace(c, ctx, selected)}`;231+${selected && repoWorkspace(c, ctx, selected)}232+</div></div>`;229233 	return adminPage(c, ctx, "repos", body);230234 }231235 @@ -310,7 +314,8 @@310314 <textarea class="issues-textarea mono" id="${repo.name}-hidden_paths" name="hidden_paths"311315 rows="4">${repo.hidden_paths}</textarea>312316 <p class="issue-meta mono">One path per line, such as notes.md or docs/private. Hidden from every page,313-the stats and tarballs, and passed to Koh for .kohignore when there is a write key.</p>317+the stats and tarballs, and passed to Koh for .kohignore when there is a write key.318+For every repo, use <a href="/admin/site#global-hide">global hide</a>.</p>314319 <button class="issues-submit" type="submit">save ${repo.name}</button>315320 </form>316321 ${renameForm(c, repo.name)}`;

view file

src/admin/site.ts+13 −6
@@ -151,6 +151,18 @@151151 <form class="issues-form" method="post" action="/admin/site" enctype="multipart/form-data">152152 ${formField(c)}153153 <input type="hidden" name="section" value="${profile ? "profile" : "site"}">154+${155+	!profile &&156+	html`<section id="global-hide">157+<h2 class="section-label mono">global hide</h2>158+<label class="issues-label" for="ignored">file and folder names</label>159+<p class="admin-intro">One file or folder name per line, such as .agent or AGENTS.md: hidden at160+any depth in every repo, from pages and tarballs. Applied after any sync under way.</p>161+<textarea class="issues-textarea mono" id="ignored" name="ignored" rows="4">${162+		fields ? (fields.ignored ?? "") : ignoredNames(ctx.kepr).join("\n")163+	}</textarea>164+</section>`165+}154166 ${FIELDS.filter(([key]) => PROFILE_FIELDS.has(key) === profile).map(input)}155167 ${156168 	!profile &&@@ -159,12 +171,7 @@159171 		(fields ? fields.support === "1" : !setting(ctx.kepr, "support_off")) && " checked"160172 	}>161173 show the support page</label>162-<label class="issues-label" for="ignored">ignored in every repo</label>163-<p class="admin-intro">One file or folder name per line, such as .agent or AGENTS.md: hidden at164-any depth in every repo, from pages and tarballs. Applied after any sync under way.</p>165-<textarea class="issues-textarea mono" id="ignored" name="ignored" rows="4">${166-		fields ? (fields.ignored ?? "") : ignoredNames(ctx.kepr).join("\n")167-	}</textarea>`174+`168175 }169176 <button class="issues-submit" type="submit">save ${profile ? "profile" : "site"}</button>170177 </form>`;

view file

tests/ignored.test.ts+11 −0
@@ -52,6 +52,17 @@5252 		}5353 	});5454 55+	test("global hide is linked from repos and precedes the long site editors", async () => {56+		const reposPage = await (await browser.get("/admin/repos")).text();57+		expect(reposPage).toContain('href="/admin/site#global-hide"');58+		const site = await (await browser.get("/admin/site")).text();59+		expect(site).toContain('id="global-hide"');60+		expect(site.indexOf('name="ignored"')).toBeLessThan(site.indexOf('name="support_page"'));61+		expect(await (await browser.get("/admin/site?section=profile")).text()).not.toContain(62+			'name="ignored"',63+		);64+	});65+5566 	test("refuses anything but a plain name", async () => {5667 		for (const name of ["src/AGENTS.md", "..", "a\u0000b", "x".repeat(256)]) {5768 			expect((await ignore(name)).status).toBe(400);

view file