Public: anyone can read this; the crew can change it.

The roadmap's linked rows carry nav, so j/k move through them and enter opens one #10667

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/10533-the-roadmap-s-linked-rows-carry-nav-so-j → main 2 commits fork

  • Merged. mason/10533-the-roadmap-s-linked-rows-carry-nav-so-j was merged into main as 73c2018.

Review

Sign in to review this change.
2 files changed +44 −0
src/web/pages/roadmap.ts +2
@@ -32,6 +32,7 @@ function needs(base: string, rows: readonly NeedsYou[]): Html | null {
3232 ${rows.map((r) =>
3333 row({
3434 title: r.text,
35 nav: true,
3536 href: `${base}/${r.pull ? "pulls" : "issues"}/${r.number}`,
3637 trail: html`${r.action}`,
3738 }),
@@ -47,6 +48,7 @@ function issueRow(base: string, i: RoadmapIssue): Html {
4748 return row({
4849 ...(i.assignee === null ? {} : { lead: avatar(i.assignee) }),
4950 title: html`#${i.number} ${i.title}${i.moved ? html`<span class="rm-moved">moved</span>` : null}`,
51 nav: true,
5052 href: `${base}/${i.pull === undefined ? "issues" : "pulls"}/${i.pull ?? i.number}`,
5153 ...(bits.length === 0 ? {} : { meta: html`${bits.join(" · ")}` }),
5254 });
test/keys-roadmap.test.ts added +42
@@ -0,0 +1,42 @@
1 /** The roadmap's linked rows join the shell's j/k traversal (#10533): one data-nav per needs-row and issue-row, none on a pillar fold. */
2 import { expect, test } from "bun:test";
3 import { roadmapBody } from "../src/web/pages/roadmap.ts";
4 import type { Roadmap, RoadmapIssue, RoadmapPillar } from "../src/web/roadmap.ts";
5
6 const issue = (number: number, extra: Partial<RoadmapIssue> = {}): RoadmapIssue => ({
7 number, title: `Issue ${number}`, assignee: null, at: "2026-11-01T00:00:00Z", moved: false, ...extra,
8 });
9
10 const pillar = (short: string, building: RoadmapIssue[], upNext: RoadmapIssue[]): RoadmapPillar => ({
11 short, name: `${short} Name`, umbrella: null, state: "current", done: 0, total: building.length + upNext.length,
12 merged: 0, movedCount: 0, building, next: null, lastLanding: null, etaDays: null, etaAt: null,
13 groups: { building, upNext, waiting: [], blocked: [], doneThisWeek: [] },
14 });
15
16 const road = (over: Partial<Roadmap>): Roadmap => ({
17 generatedAt: "2026-11-01T00:00:00Z",
18 pace: { landed24h: 0, landed7d: 0, days: 7, current: null, etaDays: null, etaAt: null },
19 needsYou: [],
20 pillars: [],
21 ...over,
22 });
23
24 const count = (s: string) => (s.match(/data-nav/g) ?? []).length;
25
26 test("each needs-row and issue-row is one data-nav row; a pillar row is none", () => {
27 const out = roadmapBody("/acme/forge", road({
28 needsYou: [
29 { kind: "hold", text: "Hold A", action: "Release", number: 5, pull: false },
30 { kind: "close", text: "Close B", action: "Close", number: 6, pull: true },
31 ],
32 pillars: [pillar("P1", [issue(1), issue(2, { pull: 9 })], [issue(3)])],
33 })).toString();
34 expect(count(out)).toBe(5);
35 const pillarRow = out.split("<li").find((li) => li.includes('id="pillar-p1"'))!;
36 expect(pillarRow.split("<li")[0]).not.toContain("data-nav");
37 expect(out).toContain("/acme/forge/pulls/6");
38 });
39
40 test("the empty page draws no data-nav row", () => {
41 expect(count(roadmapBody("/acme/forge", road({})).toString())).toBe(0);
42 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs