From fac5d60034f7c99c42f555ef19eaf190bffaec2b Mon Sep 17 00:00:00 2001 From: Claudiu Schuster Date: Fri, 18 Sep 2026 21:19:15 +0200 Subject: [PATCH] Keep the closed history open across pages and let readers choose the page size --- scripts/test-projects-ui.mjs | 11 +++++++++ site/assets/scripts/projects-v1.js | 33 +++++++++++++++++++++------ site/assets/styles/singularity-v1.css | 4 +++- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/scripts/test-projects-ui.mjs b/scripts/test-projects-ui.mjs index 97e59eb..fe8f5a4 100644 --- a/scripts/test-projects-ui.mjs +++ b/scripts/test-projects-ui.mjs @@ -184,6 +184,17 @@ test('long lists page in place with per-group page controls that reset on view c h.get('projects-list').querySelectorAll('.chip').find((n) => n.textContent === 'Open (8)').click(); await flush(); assert.equal(direct().length, 6); // a view change returns to the bounded first page assert.equal(pageLabel(h.get('projects-list')), 'Page 1 of 2'); + h.get('projects-list').querySelectorAll('.chip').find((n) => n.textContent === 'All (15)').click(); await flush(); + h.walk(history()).find((n) => n.tagName === 'SUMMARY').click(); await flush(); // open the folded history + pagerButtons(history(), /Next page/)[0].click(); await flush(); + assert.equal(history().querySelectorAll('.room-entry').length, 1); + assert.equal(history().open, true, 'turning a page keeps the history unfolded'); + const size = h.walk(h.get('projects-list')).find((n) => (n.className || '').split(/\s+/).includes('project-size')); + size.value = '24'; size.events.get('change')?.({ preventDefault() {} }); await flush(); + assert.equal(direct().length, 8); // the reader's page size is a preference, not a view + assert.equal(h.get('projects-list').querySelectorAll('.project-pager').length, 0); // one page, no controls needed + size.value = '6'; size.events.get('change')?.({ preventDefault() {} }); await flush(); + assert.equal(direct().length, 6); }); test('delivered milestones collapse to their headline but keep the record inspectable', async () => { diff --git a/site/assets/scripts/projects-v1.js b/site/assets/scripts/projects-v1.js index 00fdd95..cd3d639 100644 --- a/site/assets/scripts/projects-v1.js +++ b/site/assets/scripts/projects-v1.js @@ -59,9 +59,12 @@ let listFilter = "all"; let listQuery = ""; // Each group (open cards, closed history, closed filter) pages on its own; - // search and filter changes reset every group to its first page. + // search and filter changes reset every group to its first page. The page + // size is a reader preference and survives view changes; the history fold + // remembers its own open state so turning pages never closes it again. const listPages = new Map(); - const VISIBLE_CARDS = 6; + let pageSize = 6; + let historyOpen = false; const projectFilter = () => { const bar = node("div", undefined, "project-filter"); bar.setAttribute("role", "group"); @@ -77,6 +80,19 @@ chip.addEventListener("click", () => { listFilter = key; listPages.clear(); renderList(); }); bar.append(chip); }); + const size = node("select", undefined, "project-size"); + size.setAttribute("aria-label", "Projects per page"); + for (const value of [6, 12, 24, 48]) { + const option = node("option", String(value)); + option.value = String(value); + size.append(option); + } + size.value = String(pageSize); + size.addEventListener("change", () => { + const value = Number(size.value); + if (Number.isInteger(value) && value >= 6) { pageSize = value; listPages.clear(); renderList(); } + }); + bar.append(size); return bar; }; const renderList = () => { @@ -97,7 +113,7 @@ // the closed history folds into one inspectable group, and every group // pages in place — the list never grows exorbitantly long const pager = (group, count, anchorSelector) => { - const pages = Math.max(1, Math.ceil(count / VISIBLE_CARDS)); + const pages = Math.max(1, Math.ceil(count / pageSize)); if (pages <= 1) return null; const page = Math.min(Math.max(1, listPages.get(group) ?? 1), pages); listPages.set(group, page); @@ -125,7 +141,7 @@ element.append(back, label, next); return element; }; - return { top: bar(), bottom: bar(), slice: (entries) => entries.slice((page - 1) * VISIBLE_CARDS, page * VISIBLE_CARDS) }; + return { top: bar(), bottom: bar(), slice: (entries) => entries.slice((page - 1) * pageSize, page * pageSize) }; }; const active = shown.filter((item) => item.status !== "closed"); const archived = shown.filter((item) => item.status === "closed"); @@ -141,12 +157,15 @@ if (openPager) box.append(openPager.bottom); if (archived.length) { const history = node("details", undefined, "project-history"); - history.append(node("summary", `Closed history (${archived.length}) — show finished projects`)); + const summary = node("summary", `Closed history (${archived.length}) — show finished projects`); + // the fold keeps its open state across page turns and re-renders + summary.addEventListener("click", () => { historyOpen = !historyOpen; }); + history.append(summary); const closedPager = pager("closed", archived.length, "#projects-list .project-history"); if (closedPager) history.append(closedPager.top); history.append(...(closedPager ? closedPager.slice(archived) : archived).map(card)); if (closedPager) history.append(closedPager.bottom); - if (listQuery) history.open = true; // a search reaches the closed work directly + history.open = listQuery ? true : historyOpen; // a search reaches the closed work directly box.append(history); } } else { @@ -375,7 +394,7 @@ const setMission = (id) => { generation += 1; abort(); mission = missionId(id) ? id : null; publicDetail = null; privateDetail = null; items = []; cursor = null; loaded = false; offerMilestone = null; pending = null; writeBusy = false; - listFilter = "all"; listQuery = ""; listPages.clear(); + listFilter = "all"; listQuery = ""; listPages.clear(); historyOpen = false; if (searchInput) searchInput.value = ""; $("projects-list").replaceChildren(); $("projects-list").setAttribute("aria-busy", "false"); $("project-detail").replaceChildren(); $("project-detail").hidden = true; $("projects-more").hidden = true; diff --git a/site/assets/styles/singularity-v1.css b/site/assets/styles/singularity-v1.css index 9744aec..3afda93 100644 --- a/site/assets/styles/singularity-v1.css +++ b/site/assets/styles/singularity-v1.css @@ -160,7 +160,9 @@ .project-milestone.is-gated .room-entry-state { color: var(--room-caution); } /* --- UX pass 2026-09-18: filter the list, feel the actions --- */ -.project-filter { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 .8rem; } +.project-filter { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 .8rem; align-items: center; } +.project-size { background: color-mix(in srgb, var(--room-accent) 4%, var(--room-surface)); border: 1px solid var(--room-border); border-radius: 999px; color: var(--room-muted); cursor: pointer; font: inherit; font-size: .72rem; margin-left: .35rem; min-height: 44px; padding: 0 .7rem; transition: border-color .15s, color .15s; } +.project-size:hover, .project-size:focus { border-color: var(--room-accent); color: var(--room-text); outline: none; } .chip { font: inherit; font-size: .78rem; padding: .22rem .7rem; border-radius: 999px; border: 1px solid var(--room-border); background: transparent; color: var(--room-muted); cursor: pointer; transition: color .15s, border-color .15s, background-color .15s; } .chip:not(:disabled):hover { color: var(--room-text); border-color: var(--room-accent); } .chip.is-active { background: var(--room-accent); border-color: var(--room-accent); color: var(--room-ink, #06101d); font-weight: 600; }