The reviews table put number, title, branch, author, time, CI and approvals side by side in every row, with each status group's label inside the table, so it read as a dense grid rather than a list. Each review is now one item: a status mark and the title, with CI and approvals at the right, then a quieter line with the number, branch and target, author, commit count, diff size, when and the latest revision. Status groups get a heading with a count, like the index's shelves; merged and closed groups past ten start folded. A stacked layer hangs under the review below it, joined by a rule. On a phone, CI and approvals move under the title. j and k walk the items, and the home page's open reviews use the same list.
- commit
- 93e7cb4184d710b4dd2389c7f2ebd41c7147c081
- parent
- 8e63d4d0b4ae096eeb8e55f5e70d6a33a09b2259
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 106s, signed by cat
- kind
- refactor risk score 1.9 of 3, Jev's estimate
- review
- #6 List reviews as two-line items
ci attestation
forge-attestation 1 type ci commit 93e7cb4184d710b4dd2389c7f2ebd41c7147c081 tree 099e0e9238bbe0fca2f370b6ddf40a8fa6e81be6 script .forge/ci 38df37c0d5a6382f60569079a0dd821094e37e4a result pass exit 0 started 2026-10-18T23:37:57Z duration 106s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:d1576ee7dbafcc61ac535bfb555260f8bc6e1f9912ae2d419074c743fe95b243 241 lines |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |1a390bf running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |1a390bf pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |edc0e3f running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |edc0e3f fail in 0s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |8f8ade1 running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |8f8ade1 pass in 0s, signed |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |warning: You appear to have cloned an empty repository. |site home: deployed 4bff3cf -> /sites/ana/dotfiles/home/ |created ana/notes |remote: review #1: /ana/notes/reviews/1/ |pushed ana/todo |landing #1 A todo list: ana/todo -> main |1884ab2 running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |1884ab2 pass in 0s, signed |pushed main |2f82721 running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |2f82721 pass in 0s, signed |remote: review #7: /dami/greet/reviews/7/ |pushed dami/regions |created dami/forge |warning: You appear to have cloned an empty repository. |site guide: deployed 911602b -> /sites/dami/forge/guide/ |(node:21602) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/dotfiles in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:21726) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/notes in 0s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:21869) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/forge in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:22000) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/greet in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:22671) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built - in 0s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |226 of 226 pages render the same without 11ty |guide: 4 files, links ok |ok
Signature: good, cat
| M | site/ | 62 | +++++++++++++++++++----------- |
| M | site/ | 5 | +- |
| M | site/ | 34 | ++++++---------- |
| A | test/ | 46 | ++++++++++++++++++++++ |
@@ -66,39 +66,57 @@
6666 {% for a in r.approvals %}<span class="appr{% if not a.current %} stale{% endif %}" title="{{ a.user }} approved{% if a.rev %} r{{ a.rev }}{% endif %}{% if not a.current %}, changed since{% endif %}">✔ {{ a.user }}{% if not a.current %}{% if a.rev %} r{{ a.rev }}{% endif %}, stale{% endif %}</span>{% endfor %}6767 {% endmacro %}6868 69-{% macro reviewHead() %}70-<thead><tr><th class="num">#</th><th>Review</th><th>Branch</th><th>Author</th><th>Updated</th><th>CI</th><th>Approved</th></tr></thead>71-{% endmacro %}72-73-{# depth: how far up a stack, shown nested under the layer below. A revised74- review links what changed in its latest push. #}75-{% macro reviewRow(r, repo, depth = 0) %}69+{# One review in a list: its title on the first line with CI and approvals70+ at the right, the rest on a quieter second line. depth: how far up a71+ stack, shown nested under the layer below with a rule joining them. A72+ revised review links what changed in its latest push. #}73+{% macro reviewItem(r, repo, depth = 0) %}7674 {% css %}77-/* Review lists: a layer hangs under the one below it in the stack. */78-tr.layer .depth { display: inline-block; width: 1.4em; margin-left: calc((var(--depth) - 1) * 1.4em); }79-table.list tr:has(+ tr.layer) td { border-bottom-color: transparent; }80-@media (max-width: 720px) {81- table.list.stack tr:has(+ tr.layer) { border-bottom-color: transparent; padding-bottom: 2px; }82- table.list.stack tr.layer { padding-left: calc(8px + var(--depth) * 1.4em); }83- table.list.stack tr.layer .depth { margin-left: -1.4em; }84-}75+.rv-list { list-style: none; margin: 0; padding: 0; }76+.rv { position: relative; display: grid; grid-template-columns: auto 1fr auto; column-gap: 10px; row-gap: 3px; align-items: baseline; padding: 11px 14px; border-top: 1px solid var(--border); }77+.rv:first-child { border-top: 0; }78+.rv:hover, .rv.sel { background: var(--hover); }79+.rv.sel { box-shadow: inset 2px 0 0 var(--accent); }80+.rv-mark { grid-row: 1 / 3; align-self: start; width: 9px; height: 9px; margin-top: 6px; border-radius: 50%; background: var(--muted); }81+.rv-mark.open { background: var(--pass); }82+.rv-mark.draft { background: none; box-shadow: inset 0 0 0 2px var(--muted); }83+.rv-mark.merged { background: var(--merged); }84+.rv-mark.closed { background: var(--fail); }85+.rv-title { min-width: 0; font-weight: 600; overflow-wrap: anywhere; }86+.rv-title a::after { content: ''; position: absolute; inset: 0; }87+.rv-side { display: flex; gap: 12px; align-items: baseline; justify-content: flex-end; white-space: nowrap; font-size: 0.9em; }88+.rv-meta { grid-column: 2 / 4; display: flex; flex-wrap: wrap; column-gap: 14px; row-gap: 2px; font-size: 0.85em; color: var(--muted); }89+.rv-meta .mono { font-size: 0.95em; }90+.rv-meta .num { font-variant-numeric: tabular-nums; }91+.rv-meta .add { color: var(--pass); }92+.rv-meta .del { color: var(--fail); }93+.rv-meta a, .rv-side a { position: relative; z-index: 1; }94+.rv.layer { margin-left: calc(var(--depth) * 22px); border-left: 2px solid var(--border); }8595 a.rev { font-size: 0.85em; color: var(--muted); border: 1px solid var(--border); border-radius: 4px; padding: 0 4px; }8696 .appr { white-space: nowrap; font-weight: 600; color: var(--pass); }8797 .appr + .appr { margin-left: 8px; }8898 .appr.stale { color: var(--warn); font-weight: normal; }99+@media (max-width: 720px) {100+ .rv { grid-template-columns: auto 1fr; }101+ .rv-side { grid-column: 2; justify-content: flex-start; }102+ .rv-meta { grid-column: 2; }103+ .rv.layer { margin-left: calc(var(--depth) * 12px); }104+}89105 {% endcss %}90106 {% set last = r.revisions[r.revisions.length - 1] %}91-<tr{% if depth %} class="layer" style="--depth: {{ depth }}"{% endif %}><td class="num id">#{{ r.id }}</td><td class="main">{% if depth %}<span class="depth">↳</span>{% endif %}<a href="{{ repo.urls.review(r.id).href }}">{{ r.title }}</a></td><td class="mono hide-sm">{{ r.branch }} → {{ r.target }}</td><td class="nowrap">{{ r.author or '' }}</td><td class="date">{{ r.updated | ago }}{% if last and last.view %} <a class="rev" href="{{ repo.urls.revision(r.id, last.n).href }}" title="what changed since r{{ last.prev }}">r{{ last.n }}</a>{% endif %}</td><td>{{ reviewci(r.ci) }}</td><td class="nowrap">{{ approvals(r) }}</td></tr>107+<li class="rv{% if depth %} layer{% endif %}"{% if depth %} style="--depth: {{ depth }}"{% endif %}>108+<span class="rv-mark {{ r.status }}" title="{{ r.status }}"></span>109+<span class="rv-title"><a href="{{ repo.urls.review(r.id).href }}">{{ r.title }}</a></span>110+<span class="rv-side">{{ reviewci(r.ci) }}{% if r.approvals.length %}<span>{{ approvals(r) }}</span>{% endif %}</span>111+<span class="rv-meta"><span class="num">#{{ r.id }}</span><span class="mono">{{ r.branch }} → {{ r.target }}</span>{% if r.author %}<span>{{ r.author }}</span>{% endif %}{% if r.commits.length %}<span>{{ r.commits.length | plural('commit') }}</span>{% endif %}{% if r.combinedStats and (r.combinedStats.add or r.combinedStats.del) %}<span class="num"><span class="add">+{{ r.combinedStats.add }}</span> <span class="del">−{{ r.combinedStats.del }}</span></span>{% endif %}<span class="date">{{ r.updated | ago }}</span>{% if last and last.view %}<a class="rev" href="{{ repo.urls.revision(r.id, last.n).href }}" title="what changed since r{{ last.prev }}">r{{ last.n }}</a>{% endif %}</span>112+</li>92113 {% endmacro %}93114 94115 {# list: reviews nested by stack, as {review, depth} (nest() in lib/reviews.ts). #}95116 {% macro reviews(list, repo) %}96-<table class="list stack nav reviews">97-{{ reviewHead() }}98-<tbody>99-{% for e in list %}{{ reviewRow(e.review, repo, e.depth) }}{% endfor %}100-</tbody>101-</table>117+<ul class="rv-list nav-list">118+{% for e in list %}{{ reviewItem(e.review, repo, e.depth) }}{% endfor %}119+</ul>102120 {% endmacro %}103121 104122 {# A directory listing: folders first, then files. #}@@ -24,9 +24,10 @@
2424 [['g'], 'all repositories'],2525 ];2626 27- // Table rows, and the repo cards on the index (filtered ones are hidden).27+ // Table rows, the repo cards on the index (filtered ones are hidden), and28+ // list items such as reviews.2829 const rows = (): HTMLElement[] =>29- [...document.querySelectorAll<HTMLElement>('table.nav tbody tr, .nav-cards > li')].filter((el) => el.querySelector('a') && el.getClientRects().length);30+ [...document.querySelectorAll<HTMLElement>('table.nav tbody tr, .nav-cards > li, .nav-list > li')].filter((el) => el.querySelector('a') && el.getClientRects().length);3031 let at = -1;3132 3233 function select(i: number): void {@@ -6,18 +6,15 @@ pagination:
66 {% import "macros.njk" as m with context %}77 {% block content %}88 {% css %}9-/* Reviews by status, each status a group of rows. */10-table.list tr.group td { border: 0; padding: 18px 8px 6px; }11-table.list tr.group:hover td { background: none; }12-tr.group h3 { margin: 0; font-size: 0.95rem; }13-tr.group.open h3 { color: var(--pass); }14-tr.group.draft h3 { color: var(--muted); }15-tr.group.merged h3 { color: var(--merged); }16-tr.group.closed h3 { color: var(--fail); }17-@media (max-width: 720px) {18- table.list.stack tr.group { padding: 18px 8px 4px; border: 0; }19- table.list.stack tr.group td { font-size: 1rem; padding: 0; }20-}9+/* Reviews by status: a heading per group, the list under it. Long merged10+ and closed groups start folded. */11+.rv-group { margin: 0 0 18px; }12+.rv-group > summary { display: flex; align-items: baseline; gap: 8px; margin: 0 0 8px; cursor: pointer; list-style: none; }13+.rv-group > summary::-webkit-details-marker { display: none; }14+.rv-group > summary h2 { margin: 0; font-size: 0.78rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }15+.rv-group > summary .n { color: var(--faint); font-size: 0.78rem; }16+.rv-group > summary::after { content: ''; flex: 1; border-top: 1px solid var(--border); transform: translateY(-3px); }17+.rv-group:not([open]) > summary h2::before { content: '\25B8 '; }2118 {% endcss %}2219 {% if not p.groups.length %}2320 <div class="box"><div class="markdown">@@ -28,16 +25,11 @@ forge attest --stack # CI on your machine, signed per commit
2825 forge push -t "Title" # the branch and its CI results</pre>2926 </div></div>3027 {% else %}31-<div class="box">32-<table class="list stack nav reviews">33-{{ m.reviewHead() }}3428 {% for g in p.groups %}35-<tbody>36-<tr class="group {{ g.status }}"><td colspan="7"><h3>{{ g.status }} <span class="desc">{{ g.items.length }}</span></h3></td></tr>37-{% for e in g.items %}{{ m.reviewRow(e.review, p.repo, e.depth) }}{% endfor %}38-</tbody>29+<details class="rv-group {{ g.status }}"{% if g.status == 'open' or g.status == 'draft' or g.items.length <= 10 %} open{% endif %}>30+<summary><h2>{{ g.status }}</h2><span class="n">{{ g.items.length }}</span></summary>31+<div class="box">{{ m.reviews(g.items, p.repo) }}</div>32+</details>3933 {% endfor %}40-</table>41-</div>4234 {% endif %}4335 {% endblock %}@@ -0,0 +1,46 @@
1+# The reviews page lists reviews by status under a heading with a count,2+# each review two lines: its title (CI and approvals at the right), then3+# number, branch, author, commits, diff size and when. A stacked layer4+# hangs under the review below it. Long merged and closed groups start5+# folded; open and draft never do.6+. "$(dirname "$0")/../lib.sh"7+user dami8+user ana9+repo dami/greet10+clone dami dami/greet "$T/w/d"11+at "$T/w/d" commit "first"12+push_as dami "$T/w/d" origin main13+at "$T/w/d" git switch -q -c dami/base14+printf 'one\ntwo\n' >"$T/w/d/a.txt"15+at "$T/w/d" git add a.txt16+at "$T/w/d" git commit -q -m "base layer"17+push_as dami "$T/w/d" -o title=Base origin dami/base18+at "$T/w/d" git switch -q -c dami/top19+at "$T/w/d" commit "top layer"20+push_as dami "$T/w/d" -o title=Top origin dami/top21+# Eleven merged reviews: their group starts folded.22+i=023+while [ $i -lt 11 ]; do24+ i=$((i + 1))25+ at "$T/w/d" git switch -q -c "dami/m$i" main26+ at "$T/w/d" commit "merged $i"27+ push_as dami "$T/w/d" -o "title=Merged $i" origin "dami/m$i"28+ at "$T/w/d" git switch -q main29+ at "$T/w/d" git merge -q --ff-only "dami/m$i"30+ push_as dami "$T/w/d" origin main31+done32+export FORGE_STRICT=133+build dami/greet34+p=$FORGE_WWW/dami/greet/reviews/index.html35+lacks "$p" '<table'36+has "$p" '<details class="rv-group open" open>'37+has "$p" '<summary><h2>open</h2><span class="n">2</span></summary>'38+has "$p" '<details class="rv-group merged">'39+has "$p" '<summary><h2>merged</h2><span class="n">11</span></summary>'40+# The layer on top hangs under the base, one deep.41+has "$p" '<li class="rv layer" style="--depth: 1">'42+has "$p" '<span class="rv-title"><a href="/dami/greet/reviews/2/">Top</a></span>'43+# Base's second line: number, branch, author, commits, diff size.44+has "$p" '<span class="rv-meta"><span class="num">#1</span><span class="mono">dami/base → main</span><span>dami</span><span>1 commit</span><span class="num"><span class="add">\+2</span> <span class="del">−0</span></span>'45+# The home page's open reviews are the same list.46+has "$FORGE_WWW/dami/greet/index.html" '<ul class="rv-list nav-list">'