Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions scripts/test-projects-ui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
33 changes: 26 additions & 7 deletions site/assets/scripts/projects-v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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 = () => {
Expand All @@ -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);
Expand Down Expand Up @@ -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");
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion site/assets/styles/singularity-v1.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Loading