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
↓ kepr-7531ccf1.tar.gzatlas at this save
files changed5 files
package.json+1 −1public/css/kepr.css+50 −9src/admin/repos.ts+13 −8src/admin/site.ts+13 −6tests/ignored.test.ts+11 −0package.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": {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 {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)}`;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>`;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);