clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

Tokens, a darker dark mode, lowercase chrome and real controls

cat committed 590fe7b
Every colour, size, weight and family is a token at the top of
style.css, and the rest of it and every template's CSS use only those.
Dark mode is near black. No capitals or letter-spacing anywhere:
headings, tabs, buttons and labels are written lowercase and get their
weight from the type. Prose people wrote is in the serif.

Controls come from ui.njk: tabs for where you are (now also the
shelves, with an "all" tab), a globe icon toggle for repos with a site,
a segmented control for the sort, a checkbox for wrapping lines,
buttons that do things.
commit
590fe7bb3cf907c66fe24c329cb83e29d9755888
parent
7814de5675c77b0724b49c0462437bb5fe09c0f8
author
cat <admin@dev.site>
ci
✓ passed in 76s, signed by cat
kind
refactor risk score 2.5 of 3, Jev's estimate
review
#13 A design system: tokens, three fonts, no caps, real controls
ci attestation
forge-attestation 1
type ci
commit 590fe7bb3cf907c66fe24c329cb83e29d9755888
tree d565a2e9af1d7a0b115b9972dad71f203722730a
script .forge/ci 38df37c0d5a6382f60569079a0dd821094e37e4a
result pass
exit 0
started 2026-10-19T02:57:55Z
duration 76s
runner (not shown here; refs/notes/ci has it)
system Darwin 25.5.0 arm64
env host
log sha256:8944aba97903d9b2ff0a75c1271b4f178f5dcdd41ad73b478cf7f2eeca1948e9 253 lines
|remote: review #4: /dami/greet/reviews/4/
|pushed wip/i18n
|ed3f7ac running .forge/ci: greet.sh: say what it is
|  | sh: /bin/sh
|  | ok   greet
|ed3f7ac pass in 0s, signed
|pushed main
|remote: review #4: /dami/greet/reviews/4/
|pushed wip/i18n
|de6c7d4 running .forge/ci: wave at the world
|  | sh: /bin/sh
|  | FAIL greet : got 'hello, world o/', want 'hello, world'
|de6c7d4 fail in 0s, signed
|forge: 1 commit(s) did not pass
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|9831599 running .forge/ci: update the test for the wave
|  | sh: /bin/sh
|  | ok   greet
|9831599 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 076a719 -> /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
|f149f94 running .forge/ci: web, server and Python versions of greet
|  | sh: /bin/sh
|  | ok   greet
|f149f94 pass in 0s, signed
|pushed main
|fd0726f running .forge/ci: messages: notes for the later regions
|  | sh: /bin/sh
|  | ok   greet
|fd0726f 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 7c4a163 -> /sites/dami/forge/guide/
|(node:31430) 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 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.NUNlre/wt/demo/root/.forge/www
|(node:31558) 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 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.NUNlre/wt/demo/root/.forge/www
|(node:31703) 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 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.NUNlre/wt/demo/root/.forge/www
|(node:31836) 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 2s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.NUNlre/wt/demo/root/.forge/www
|(node:32800) 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 -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.NUNlre/wt/demo/root/.forge/www
|226 of 226 pages render the same without 11ty
|guide: 4 files, links ok
|ok

Signature: good, cat

40 files changed, +732 -517
Mlib/jev-commits.ts2+-
Msite/_includes/base.njk29+-
Msite/_includes/discussion.njk43+-
Msite/_includes/help.njk22+-
Msite/_includes/insights-body.njk86++---
Msite/_includes/login.njk12+-
Msite/_includes/macros.njk101++++---
Msite/_includes/private.njk9+-
Msite/_includes/search.njk45+-
Asite/_includes/ui.njk93++++++
Msite/assets/diffs.ts10+-
Msite/assets/discuss.ts12+-
Msite/assets/insights.ts22+-
Msite/assets/keys.ts2+-
Msite/assets/logo.svg2+-
Msite/assets/search.ts22+-
Msite/assets/style.css483++++++++++++++++++------------
Msite/assets/ui.ts45++-
Msite/index.njk1+
Msite/insights.njk1+
Msite/owner.njk1+
Msite/repo/blob.njk7+-
Msite/repo/commit.njk21+-
Msite/repo/home.njk3+-
Msite/repo/insights.njk3+-
Msite/repo/log.njk1+
Msite/repo/refs.njk18+-
Msite/repo/releases.njk15+-
Msite/repo/review-diff.njk34+-
Msite/repo/review.njk38+-
Msite/repo/reviews.njk10+-
Msite/repo/revision.njk21+-
Msite/repo/sites.njk3+-
Msite/repo/tree.njk3+-
Msite/repo/wiki.njk17+-
Mtest/scenarios/insights-pages.sh2+-
Mtest/scenarios/log-columns.sh2+-
Mtest/scenarios/release-page.sh4+-
Mtest/scenarios/release-tab.sh2+-
Mtest/scenarios/web-comment-page.sh2+-
M lib/jev-commits.ts +1 -1
M site/_includes/base.njk +15 -14
@@ -2,6 +2,7 @@
22 replace head (its description and link preview), scripts (any script only33 it needs) and heading (the title in the header). -#}44 {% import "macros.njk" as m with context %}5+{% import "ui.njk" as ui %}56 {% set r = p.repo if p else null %}67 {% set br = p.branch.name if p and p.branch else (r.defaultBranch if r else '') %}78 <!DOCTYPE html>
@@ -34,7 +35,7 @@
3435 <a class="logo" href="/" data-key="g" title="{{ forge.config.name }}: all repositories" aria-label="{{ forge.config.name }}">{% include "logo.njk" %}</a>3536 <div class="title">3637 {% block heading %}37-{% if r %}<a href="/{{ r.owner }}/">{{ r.owner }}</a> / <a href="{{ r.urls.home.href }}" data-key="h"><b>{{ r.name }}</b></a>{% if r.private %} <a class="private-mark" href="/private/" title="A private repo: only its readers see its pages">private</a>{% endif %}38+{% if r %}<a href="/{{ r.owner }}/">{{ r.owner }}</a> / <a href="{{ r.urls.home.href }}" data-key="h"><b>{{ r.name }}</b></a>{% if r.private %} <a class="private-mark" href="/private/" title="a private repo: only its readers see its pages">private</a>{% endif %}3839 {% else %}<b>{{ forge.config.name }}</b>{% endif %}3940 {% endblock %}4041 </div>
@@ -43,10 +44,10 @@
4344 {% include "search.njk" %}4445 {% if r %}4546 <details class="clone">46-<summary>Clone</summary>47+<summary>clone</summary>4748 <div class="panel">48-<div class="copyrow"><label>SSH</label><input readonly value="{{ r.urls.cloneSsh }}" aria-label="ssh clone url"><button type="button" data-copy="{{ r.urls.cloneSsh }}">Copy</button></div>49-{% if r.urls.cloneHttp %}<div class="copyrow"><label>HTTPS</label><input readonly value="{{ r.urls.cloneHttp }}" aria-label="https clone url"><button type="button" data-copy="{{ r.urls.cloneHttp }}">Copy</button></div>{% endif %}49+<div class="copyrow"><label>ssh</label>{{ ui.input('ssh clone url', r.urls.cloneSsh, 'readonly', true) }}{{ ui.button('copy', false, 'data-copy="' + r.urls.cloneSsh + '"') }}</div>50+{% if r.urls.cloneHttp %}<div class="copyrow"><label>https</label>{{ ui.input('https clone url', r.urls.cloneHttp, 'readonly', true) }}{{ ui.button('copy', false, 'data-copy="' + r.urls.cloneHttp + '"') }}</div>{% endif %}5051 {% if r.private %}<p class="clone-note">HTTPS takes a token: <code>ssh {{ forge.config.ssh }} token --repo {{ r.slug }}</code></p>{% endif %}5152 </div>5253 </details>
@@ -54,16 +55,16 @@
5455 </div>5556 {% if r and r.description %}<p class="repo-desc">{{ r.description }}</p>{% endif %}5657 {% if r %}57-<nav class="tabs" aria-label="repository">58-<a href="{{ r.urls.home.href }}" data-key="f"{% if p.nav == 'files' or p.nav == 'home' %} class="cur"{% endif %}>Code</a>59-<a href="{{ r.urls.log(br).href }}" data-key="l"{% if p.nav == 'log' %} class="cur"{% endif %}>Commits</a>60-<a href="{{ r.urls.refs.href }}" data-key="b"{% if p.nav == 'refs' %} class="cur"{% endif %}>Branches</a>61-<a href="{{ r.urls.reviews.href }}" data-key="r"{% if p.nav == 'reviews' %} class="cur"{% endif %}>Reviews</a>62-{% if r.counts.wiki %}<a href="{{ r.urls.wiki().href }}" data-key="w"{% if p.nav == 'wiki' %} class="cur"{% endif %}>Wiki</a>{% endif %}63-{% if r.counts.sites %}<a href="{{ r.urls.sites.href }}" data-key="s"{% if p.nav == 'sites' %} class="cur"{% endif %}>Sites</a>{% endif %}64-{% if r.counts.releases %}<a href="{{ r.urls.releases.href }}" data-key="e"{% if p.nav == 'releases' %} class="cur"{% endif %}>Releases</a>{% endif %}65-<a href="{{ r.urls.insights.href }}" data-key="i"{% if p.nav == 'insights' %} class="cur"{% endif %}>Insights</a>66-</nav>58+{{ ui.tabs('repository', [59+ {href: r.urls.home.href, text: 'code', key: 'f', cur: p.nav == 'files' or p.nav == 'home'},60+ {href: r.urls.log(br).href, text: 'commits', key: 'l', cur: p.nav == 'log'},61+ {href: r.urls.refs.href, text: 'branches', key: 'b', cur: p.nav == 'refs'},62+ {href: r.urls.reviews.href, text: 'reviews', key: 'r', cur: p.nav == 'reviews'},63+ {href: r.urls.wiki().href if r.counts.wiki else '', text: 'wiki', key: 'w', cur: p.nav == 'wiki', off: not r.counts.wiki},64+ {href: r.urls.sites.href, text: 'sites', key: 's', cur: p.nav == 'sites', off: not r.counts.sites},65+ {href: r.urls.releases.href, text: 'releases', key: 'e', cur: p.nav == 'releases', off: not r.counts.releases},66+ {href: r.urls.insights.href, text: 'insights', key: 'i', cur: p.nav == 'insights'}67+]) }}6768 {% endif %}6869 </div>6970 </header>
M site/_includes/discussion.njk +21 -22
@@ -3,40 +3,39 @@
33 revision are outdated. Written with forge comment and forge approve, or44 from this page signed in: the form at the end stays hidden until55 discuss.ts finds who is signed in, and posts the tip the page shows. #}6+{% import "ui.njk" as ui %}67 {% set u = p.repo.urls %}78 {% css %}8-ul.approvals { list-style: none; margin: 0; padding: 10px 0; border-bottom: 1px solid var(--border); display: flex; flex-wrap: wrap; gap: 4px 18px; font-size: 0.92em; }9+ul.approvals { list-style: none; margin: 0; padding: var(--space-3) 0; border-bottom: var(--hairline) solid var(--border); display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-5); font-size: var(--text-sm); }910 ul.approvals .tick { color: var(--pass); }1011 ul.approvals .stale { color: var(--muted); }1112 ul.approvals .stale .tick { color: var(--warn); }12-ul.approvals .date { font: 12px var(--mono); color: var(--muted); }13-ul.approvals .date time { font-size: 1em; }13+ul.approvals .date { font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--muted); }14+ul.approvals .date time { font-size: inherit; }1415 ol.comments { list-style: none; margin: 0; padding: 0; }15-.comment { border-bottom: 1px solid var(--border); padding: 0 0 4px; }16-.comment:target { box-shadow: inset 3px 0 0 var(--accent); }17-.comment-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; padding: 10px 12px 0; font-size: 0.9em; }18-.comment-head .date { font: 12px var(--mono); color: var(--muted); }19-.comment-head .date time { font-size: 1em; }20-.comment-head .rev { font: 12px var(--mono); color: var(--muted); }21-.comment .tag { font: 600 10.5px var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--warn); }22-.comment .quote { margin: 8px 12px 0; border: 1px solid var(--border); background: var(--raised); overflow: hidden; }23-.comment .quote .where { padding: 3px 10px; font: 12px/1.6 var(--mono); border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }24-.comment .quote pre { margin: 0; padding: 4px 10px; font: 12.5px/1.5 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; tab-size: 4; }16+.comment { border-bottom: var(--hairline) solid var(--border); padding: 0 0 var(--space-1); }17+.comment:target { box-shadow: inset var(--bar) 0 0 var(--accent); }18+.comment-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-half) var(--space-3); padding: var(--space-3) var(--space-3) 0; font-size: var(--text-sm); }19+.comment-head .date { font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--muted); }20+.comment-head .date time { font-size: inherit; }21+.comment-head .rev { font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--muted); }22+.comment .tag { font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--warn); }23+.comment .quote { margin: var(--space-2) var(--space-3) 0; border: var(--hairline) solid var(--border); background: var(--raised); overflow: hidden; }24+.comment .quote .where { padding: var(--space-1) var(--space-3); font: var(--fw-regular) var(--text-xs)/1.6 var(--mono); border-bottom: var(--hairline) solid var(--border); overflow-wrap: anywhere; }25+.comment .quote pre { margin: 0; padding: var(--space-1) var(--space-3); font: var(--fw-regular) var(--text-code)/1.55 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; tab-size: 4; }2526 .comment.outdated .quote pre { color: var(--muted); }26-.comment .markdown { padding: 6px 12px 8px; font-size: 0.95rem; line-height: 1.55; }27+.comment .markdown { padding: var(--space-2) var(--space-3); font-size: var(--text-md); line-height: 1.6; }2728 .comment .markdown > :last-child { margin-bottom: 0; }2829 /* Sent from this page, until the rebuilt page shows it. */2930 .comment.pending { border-bottom-style: dashed; }30-.comment.pending .text { padding: 8px 12px; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--muted); }31-.reply { margin: 12px 0 0; }32-.reply textarea { display: block; width: 100%; min-height: 6em; resize: vertical; font: 14px/1.5 var(--sans); color: var(--fg); background: var(--raised); border: 1px solid var(--border); padding: 8px 10px; }33-.reply textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }34-.reply-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; margin-top: 8px; font-size: 0.92em; }31+.comment.pending .text { padding: var(--space-2) var(--space-3); white-space: pre-wrap; overflow-wrap: anywhere; color: var(--muted); }32+.reply { margin: var(--space-3) 0 0; }33+.reply-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-2); font-size: var(--text-sm); }3534 .reply-bar .grow { flex: 1; }3635 .reply-status.bad { color: var(--fail); white-space: pre-line; }3736 {% endcss %}3837 <section class="discussion" id="discussion">39-<h2>Discussion <span class="desc">{{ rv.comments.length | plural('comment') }}{% if rv.approvals.length %}, {{ rv.approvals.length | plural('approval') }}{% endif %}</span></h2>38+{{ ui.heading('discussion', null, (rv.comments.length | plural('comment')) + ((', ' + (rv.approvals.length | plural('approval'))) if rv.approvals.length else '')) }}4039 {% if rv.approvals.length %}4140 <ul class="approvals">4241 {% for a in rv.approvals %}
@@ -61,7 +60,7 @@ ol.comments { list-style: none; margin: 0; padding: 0; }
6160 <p class="desc" id="no-comments">No comments yet: <code>forge comment {{ rv.id }}</code> adds one.</p>6261 {% endif %}6362 <form class="reply" id="reply" data-post="{{ u.review(rv.id).href }}" data-at="{{ rv.tip }}" data-author="{{ rv.author or '' }}" hidden>64-<textarea name="body" maxlength="32768" aria-label="Comment on #{{ rv.id }}" placeholder="Comment on #{{ rv.id }}. Markdown is fine."></textarea>65-<div class="reply-bar"><span class="desc who"></span><span class="grow"></span><span class="reply-status" role="status"></span>{% if rv.live %}<button type="button" data-approve title="Approve this tree of #{{ rv.id }}, as forge approve {{ rv.id }} does">Approve</button>{% endif %}<button type="submit" class="go">Comment</button></div>63+{{ ui.textarea('comment on #' + rv.id, 'Comment on #' + rv.id + '. Markdown is fine.', 'name="body" maxlength="32768"') }}64+<div class="reply-bar"><span class="desc who"></span><span class="grow"></span><span class="reply-status" role="status"></span>{% if rv.live %}{{ ui.button('approve', false, 'data-approve title="approve this tree of #' + rv.id + ', as forge approve ' + rv.id + ' does"') }}{% endif %}{{ ui.button('comment', true, '', 'submit') }}</div>6665 </form>6766 </section>
M site/_includes/help.njk +11 -11
@@ -1,18 +1,18 @@
11 {# The footer, with the button that lists the keyboard shortcuts in a dialog (keys.ts). -#}22 {% css %}3-footer.site .keyhelp { background: none; border: 0; padding: 0; color: var(--muted); }3+footer.site .keyhelp { min-height: 0; background: none; border: 0; padding: 0; color: var(--muted); }44 footer.site .keyhelp:hover { color: var(--fg); background: none; }55 @media (pointer: coarse) and (hover: none) { footer.site .keyhelp { display: none; } }66 /* keyboard shortcuts (keys.ts) */7-#help { width: min(440px, 94vw); padding: 0; background: var(--raised); color: var(--fg); border: 1px solid var(--border); box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2); }7+#help { width: min(440px, 94vw); padding: 0; background: var(--raised); color: var(--fg); border: var(--hairline) solid var(--border); box-shadow: var(--shadow); }88 #help:focus { outline: none; }9-#help::backdrop { background: rgba(0, 0, 0, 0.3); }10-#help > div { padding: 16px 20px; }11-#help h2 { margin: 0 0 4px; }12-#help table { width: 100%; border-collapse: collapse; font-size: 0.92em; }13-#help td { padding: 5px 0; vertical-align: top; border-bottom: 1px solid var(--border); }14-#help td:first-child { width: 1%; padding-right: 16px; white-space: nowrap; }15-#help kbd { color: var(--fg); font-size: 0.85em; }16-#help p { margin: 12px 0 0; font-size: 0.85em; }9+#help::backdrop { background: var(--scrim); }10+#help > div { padding: var(--space-4) var(--space-5); }11+#help h2 { margin: 0 0 var(--space-1); }12+#help table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }13+#help td { padding: var(--space-1) 0; vertical-align: top; border-bottom: var(--hairline) solid var(--border); }14+#help td:first-child { width: 1%; padding-right: var(--space-4); white-space: nowrap; }15+#help kbd { color: var(--fg); }16+#help p { margin: var(--space-3) 0 0; font-size: var(--text-sm); }1717 {% endcss %}18-<footer class="site wrap"><button type="button" class="keyhelp" title="Keyboard shortcuts"><kbd>?</kbd> shortcuts</button></footer>18+<footer class="site wrap"><button type="button" class="keyhelp" title="keyboard shortcuts"><kbd>?</kbd> shortcuts</button></footer>
M site/_includes/insights-body.njk +36 -50
@@ -5,37 +5,22 @@
55 {% set forgeWide = not p %}66 {% set base = '/insights/' if forgeWide else p.repo.urls.insights.href %}77 {% css %}8-/* insights (insights.ts): the chart colors are the data-viz palette's,9- checked for color-blind readers against this page's light and dark10- backgrounds; text keeps the page's own colors. */11-.insights {12- --viz-1: #3148f6;13- --viz-fail: #c7162b;14- --viz-grid: #d9d9d9;15- --viz-axis: #a9aeb4;16-}17-@media (prefers-color-scheme: dark) {18- .insights {19- --viz-1: #6f7fff;20- --viz-fail: #f0606e;21- --viz-grid: #2a3037;22- --viz-axis: #4a525b;23- }24-}25-.insights .ins-meta { margin: 0.4em 0 1em; }26-.insights > h3 { margin: 28px 0 0; padding-bottom: 6px; border-bottom: 1px solid var(--border); font: 600 11px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }27-.insights .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0 20px; margin: 0 0 8px; }28-.insights .kpi { border-top: 1px solid var(--border); padding: 10px 0; }29-.insights .kpi .label { font: 600 10.5px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); }30-.insights .kpi .value { font: 600 1.5rem/1.2 var(--mono); margin-top: 4px; font-variant-numeric: tabular-nums; }31-.insights .kpi .sub { color: var(--muted); font-size: 0.85em; }32-.insights .charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 440px), 1fr)); gap: 12px 20px; }8+/* insights (insights.ts): the chart colours are tokens (--viz-*, checked9+ for colour-blind readers against both pages); text keeps the page's own. */10+.insights .ins-meta { margin: var(--space-1) 0 var(--space-4); }11+.insights > h3 { margin: var(--space-8) 0 0; padding-bottom: var(--space-2); border-bottom: var(--hairline) solid var(--border); font-size: var(--text-md); }12+.insights .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0 var(--space-5); margin: 0 0 var(--space-2); }13+.insights .kpi { border-top: var(--hairline) solid var(--border); padding: var(--space-3) 0; }14+.insights .kpi .label { font-size: var(--text-sm); color: var(--muted); }15+.insights .kpi .value { font: var(--fw-medium) var(--text-xl)/1.2 var(--mono); margin-top: var(--space-1); font-variant-numeric: tabular-nums; }16+.insights .kpi .sub { color: var(--muted); font-size: var(--text-sm); }17+.insights .charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 440px), 1fr)); gap: var(--space-3) var(--space-5); }3318 .insights figure { margin: 0; min-width: 0; position: relative; }34-.insights figcaption h3 { margin: 1.6em 0 2px; font: 600 11px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }35-.insights figcaption .desc { font-size: 0.85em; }36-.insights .plot { position: relative; margin-top: 6px; }19+.insights figcaption h3 { margin: var(--space-6) 0 var(--space-half); font-size: var(--text-md); }20+.insights figcaption .desc { font-size: var(--text-sm); }21+.insights .plot { position: relative; margin-top: var(--space-2); }3722 .insights .plot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }38-.insights svg { display: block; width: 100%; overflow: visible; font: 10.5px var(--mono); font-variant-numeric: tabular-nums; }23+.insights svg { display: block; width: 100%; overflow: visible; font: var(--fw-regular) var(--text-xs) var(--mono); font-variant-numeric: tabular-nums; }3924 .insights svg .grid { stroke: var(--viz-grid); stroke-width: 1; shape-rendering: crispEdges; }4025 .insights svg .base { stroke: var(--viz-axis); stroke-width: 1; shape-rendering: crispEdges; }4126 .insights svg .tick { fill: var(--muted); }
@@ -45,20 +30,21 @@
4530 .insights svg .hair { stroke: var(--muted); stroke-width: 1; shape-rendering: crispEdges; }4631 .insights svg .on { filter: brightness(1.15); }4732 .insights svg .mark-label { fill: var(--fg); paint-order: stroke; stroke: var(--bg); stroke-width: 3px; stroke-linejoin: round; }48-.insights .empty-chart { color: var(--muted); font-size: 0.9em; padding: 28px 0; text-align: center; border-block: 1px solid var(--border); }49-.insights .legend { display: flex; gap: 14px; font-size: 0.85em; color: var(--muted); margin-top: 4px; }50-.insights .legend i { display: inline-block; width: 10px; height: 10px; margin-right: 5px; vertical-align: -1px; }51-.insights .tip { position: absolute; z-index: 2; pointer-events: none; background: var(--raised); border: 1px solid var(--border); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); padding: 6px 9px; font-size: 0.85em; white-space: nowrap; }52-.insights .tip b { font-size: 1.05em; }53-.insights .tip .k { display: inline-block; width: 10px; height: 2px; margin-right: 6px; vertical-align: middle; }54-.insights details.table { margin-top: 4px; font-size: 0.85em; }55-.insights details.table summary { color: var(--muted); cursor: pointer; font: 600 10.5px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; }56-.insights details.table table { border-collapse: collapse; margin-top: 4px; font-variant-numeric: tabular-nums; }57-.insights details.table td, .insights details.table th { padding: 2px 10px 2px 0; text-align: left; border-bottom: 1px solid var(--border); }33+.insights .empty-chart { color: var(--muted); font-size: var(--text-sm); padding: var(--space-8) 0; text-align: center; border-block: var(--hairline) solid var(--border); }34+.insights .legend { display: flex; gap: var(--space-4); font-size: var(--text-sm); color: var(--muted); margin-top: var(--space-1); }35+.insights .legend i { display: inline-block; width: 10px; height: 10px; margin-right: var(--space-1); vertical-align: -1px; }36+.insights .tip { position: absolute; z-index: 2; pointer-events: none; background: var(--raised); border: var(--hairline) solid var(--border); box-shadow: var(--shadow); padding: var(--space-1) var(--space-2); font-size: var(--text-sm); white-space: nowrap; }37+.insights .tip b { font-size: var(--text-md); }38+.insights .tip .k { display: inline-block; width: 10px; height: 2px; margin-right: var(--space-2); vertical-align: middle; }39+.insights details.table { margin-top: var(--space-1); font-size: var(--text-sm); }40+.insights details.table summary { color: var(--muted); cursor: pointer; font-weight: var(--fw-medium); }41+.insights details.table table { border-collapse: collapse; margin-top: var(--space-1); font-variant-numeric: tabular-nums; }42+.insights details.table td, .insights details.table th { padding: var(--space-half) var(--space-3) var(--space-half) 0; text-align: left; border-bottom: var(--hairline) solid var(--border); }43+.insights details.table th { font-weight: var(--fw-medium); color: var(--muted); }5844 .insights details.table .scroll { max-height: 240px; overflow: auto; }59-.insights .files td { vertical-align: top; padding: 1px 12px 1px 0; }45+.insights .files td { vertical-align: top; padding: var(--space-half) var(--space-3) var(--space-half) 0; }6046 .insights .files td:first-child { white-space: nowrap; }61-.insights pre.cmd { overflow-x: auto; background: var(--subtle); border: 1px solid var(--border); padding: 8px 12px; font-size: 0.85em; }47+.insights pre.cmd { overflow-x: auto; background: var(--raised); border: var(--hairline) solid var(--border); padding: var(--space-2) var(--space-3); font-size: var(--text-sm); }6248 @media (forced-colors: active) {6349 .insights svg .s1, .insights svg .fail { fill: CanvasText; }6450 }
@@ -67,23 +53,23 @@
6753 <p class="desc ins-meta" id="ins-meta">From the forge's lake (<code>forge lake</code>).<noscript> The charts need JavaScript; the numbers are in the files below.</noscript></p>6854 <div class="kpis" id="ins-kpis"></div>6955 <div class="charts">70-<figure data-chart="builds"><figcaption><h3>Build time</h3><span class="desc">seconds per build of {{ 'the pages' if forgeWide else "this repo's pages" }}</span></figcaption><div class="plot"></div><details class="table"><summary>Table</summary></details></figure>71-<figure data-chart="pushes"><figcaption><h3>Pushes</h3><span class="desc">pushes a <span class="ins-bucket">day</span></span></figcaption><div class="plot"></div><details class="table"><summary>Table</summary></details></figure>72-<figure data-chart="land"><figcaption><h3>Time to land</h3><span class="desc">hours from a review's first push to landing, by when it landed</span></figcaption><div class="plot"></div><details class="table"><summary>Table</summary></details></figure>73-<figure data-chart="revisions"><figcaption><h3>Revisions per review</h3><span class="desc">reviews by how many revisions they were pushed in</span></figcaption><div class="plot"></div><details class="table"><summary>Table</summary></details></figure>74-<figure data-chart="ci"><figcaption><h3>CI results</h3><span class="desc">attestations a <span class="ins-bucket">day</span>, passed and failed</span></figcaption><div class="plot"></div><details class="table"><summary>Table</summary></details></figure>75-<figure data-chart="ciTime"><figcaption><h3>CI duration</h3><span class="desc">seconds per run of <code>.forge/ci</code></span></figcaption><div class="plot"></div><details class="table"><summary>Table</summary></details></figure>56+<figure data-chart="builds"><figcaption><h3>build time</h3><span class="desc">seconds per build of {{ 'the pages' if forgeWide else "this repo's pages" }}</span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>57+<figure data-chart="pushes"><figcaption><h3>pushes</h3><span class="desc">pushes a <span class="ins-bucket">day</span></span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>58+<figure data-chart="land"><figcaption><h3>time to land</h3><span class="desc">hours from a review's first push to landing, by when it landed</span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>59+<figure data-chart="revisions"><figcaption><h3>revisions per review</h3><span class="desc">reviews by how many revisions they were pushed in</span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>60+<figure data-chart="ci"><figcaption><h3>ci results</h3><span class="desc">attestations a <span class="ins-bucket">day</span>, passed and failed</span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>61+<figure data-chart="ciTime"><figcaption><h3>ci duration</h3><span class="desc">seconds per run of <code>.forge/ci</code></span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>7662 </div>7763 {% if forgeWide %}78-<h3>By repo</h3>64+<h3>by repo</h3>7965 <div class="box">8066 <table class="list nav" id="ins-repos">81-<thead><tr><th>Repository</th><th class="num">Pushes</th><th class="num">Builds</th><th class="num">Median build</th><th class="num">Landed</th><th class="num">CI passed</th><th>Last push</th></tr></thead>67+<thead><tr><th>repository</th><th class="num">pushes</th><th class="num">builds</th><th class="num">median build</th><th class="num">landed</th><th class="num">ci passed</th><th>last push</th></tr></thead>8268 <tbody></tbody>8369 </table>8470 </div>8571 {% endif %}86-<h3>Data</h3>72+<h3>data</h3>8773 <p class="desc">The rows behind the charts{{ ', for public repos only' if forgeWide else '' }}, as Parquet. DuckDB reads them over HTTP in byte ranges, so a query fetches only what it needs:</p>8874 <table class="files desc">8975 <tr><td><a href="{{ base }}insights.json">insights.json</a></td><td>what the charts draw</td></tr>
M site/_includes/login.njk +6 -6
@@ -4,6 +4,7 @@
44 out. Data: forge, p (title), user (or ''), csrf, signal (JSON or ''),55 next, ready (sign-in is set up: FORGE_BASE_URL), host. -#}66 {% extends "base.njk" %}7+{% import "ui.njk" as ui %}78 {% block scripts %}89 <script src="{{ assets.webauthn }}" defer></script>910 <script src="{{ assets.login }}" defer></script>
@@ -13,9 +14,8 @@
1314 {% block content %}1415 {% css %}1516 /* the sign-in page */16-.login { padding: 0 0 16px; max-width: 40em; }17-.login h1 { font-size: 1.3rem; margin: 4px 0 12px; }18-.login button.go { padding: 7px 14px; }17+.login { padding: 0 0 var(--space-4); max-width: 40em; }18+.login h1 { font-size: var(--text-xl); margin: var(--space-1) 0 var(--space-3); }1919 .login-status { min-height: 1.4em; }2020 .login-status.bad { color: var(--fail); }2121 .login form { display: inline; }
@@ -24,7 +24,7 @@
2424 {% if user %}2525 <h1>Signed in as {{ user }}</h1>2626 <p>You see the pages of the private repos you can read: <a href="/private/">your private repos</a>.</p>27-<form method="post" action="/login/logout"><input type="hidden" name="csrf" value="{{ csrf }}"><button type="submit" id="signout">Sign out</button></form>27+<form method="post" action="/login/logout"><input type="hidden" name="csrf" value="{{ csrf }}">{{ ui.button('sign out', false, 'id="signout"', 'submit') }}</form>2828 {% elif not ready %}2929 <h1>Sign in</h1>3030 <p>Sign-in isn't set up here: the forge needs <code>FORGE_BASE_URL</code>, the origin passkeys belong to.</p>
@@ -32,12 +32,12 @@
3232 <div id="signin-part">3333 <h1>Sign in</h1>3434 <p>Private repos show their pages to their readers. Sign in with a passkey you have added to see them.</p>35-<p><button type="button" class="go" id="signin">Sign in with a passkey</button></p>35+<p>{{ ui.button('sign in with a passkey', true, 'id="signin"') }}</p>3636 </div>3737 <div id="enroll-part" hidden>3838 <h1>Add a passkey</h1>3939 <p>For <b id="enroll-user">you</b> on {{ host }}. Your browser, phone or password manager keeps it, and signs you in with it from now on.</p>40-<p><button type="button" class="go" id="enroll">Add a passkey</button></p>40+<p>{{ ui.button('add a passkey', true, 'id="enroll"') }}</p>4141 </div>4242 <p class="login-status" id="login-status" role="status"></p>4343 <p class="desc">No passkey yet, or lost them all? <code>ssh {{ forge.config.ssh }} passkey</code> prints a link that adds one: your ssh key is how you get in.</p>
M site/_includes/macros.njk +58 -43
@@ -1,5 +1,7 @@
11 {# Shared markup. Import it with context, so the CSS blocks in these macros2- reach the page's bundle: {% import "macros.njk" as m with context %} #}2+ reach the page's bundle: {% import "macros.njk" as m with context %}3+ The controls themselves are in ui.njk. #}4+{% import "ui.njk" as ui %}35 46 {# A page's description and its link preview in chat apps (OpenGraph).57 og:url needs the forge's address, FORGE_BASE_URL. #}
@@ -40,7 +42,7 @@
4042 {% macro log(commits, repo) %}4143 {% set jev = commits | selectattr('jev') | length %}4244 <table class="list stack nav log">43-<thead><tr>{% if jev %}<th class="kind">Type</th>{% endif %}<th>Commit</th><th>Author</th><th>Date</th><th class="num">+</th><th class="num">-</th>{% if jev %}<th class="num" title="Jev's estimate of how likely a change is to break something, 0 to 3">Risk</th>{% endif %}<th>CI</th></tr></thead>45+<thead><tr>{% if jev %}<th class="kind">type</th>{% endif %}<th>commit</th><th>author</th><th>date</th><th class="num">+</th><th class="num">-</th>{% if jev %}<th class="num" title="Jev's estimate of how likely a change is to break something, 0 to 3">risk</th>{% endif %}<th>ci</th></tr></thead>4446 <tbody>4547 {% for c in commits %}4648 <tr>{% if jev %}<td class="kind">{{ jevKind(c) }}</td>{% endif %}<td class="main"><a href="{{ repo.urls.commit(c.sha).href }}">{{ c.subject }}</a>{{ jevMismatch(c) }}</td><td class="nowrap">{{ c.author.name }}</td><td class="date">{{ c.author.date | date }}</td><td class="num i">+{{ c.stats.add }}</td><td class="num d">-{{ c.stats.del }}</td>{% if jev %}<td class="num risk">{{ jevRisk(c) }}</td>{% endif %}<td>{{ c.ci | ciHtml | safe }}</td></tr>
@@ -53,7 +55,7 @@
5355 new branch doesn't change every page. Without JS it links to the branches page. #}5456 {% macro branches(repo, current, kind) %}5557 <details class="picker" fx-action="{{ repo.urls.branchMenu(kind).href }}" fx-trigger="toggle" fx-target="#branch-menu" fx-swap="innerHTML">56-<summary>&#9095; <b>{{ current }}</b></summary>58+<summary>{{ ui.icon('branch') }}<b>{{ current }}</b></summary>5759 <div id="branch-menu"><a href="{{ repo.urls.refs.href }}">all branches</a></div>5860 </details>5961 {% endmacro %}
@@ -62,7 +64,7 @@
6264 <span class="crumbs"><a href="{{ repo.urls.tree(branch).href }}">{{ repo.name }}</a>{% for c in list %} / <a href="{{ c.href }}">{{ c.name }}</a>{% endfor %}</span>6365 {% endmacro %}6466 65-{% macro status(s) %}<span class="status {{ s }}">{{ s }}</span>{% endmacro %}67+{% macro status(s) %}{{ ui.status(s) }}{% endmacro %}6668 6769 {% macro reviewci(ci) %}6870 {% if ci %}<span class="ci {{ ci.state }}" title="{{ ci.pass }} of {{ ci.total }} commits passed">{{ '✓' if ci.state == 'pass' else ('✗' if ci.state == 'fail' else '–') }} {{ ci.pass }}/{{ ci.total }}</span>{% else %}<span class="desc">no CI</span>{% endif %}
@@ -80,29 +82,31 @@
8082 {% macro reviewItem(r, repo, depth = 0) %}8183 {% css %}8284 .rv-list { list-style: none; margin: 0; padding: 0; }83-.rv { position: relative; display: grid; grid-template-columns: auto 1fr auto; column-gap: 10px; row-gap: 3px; align-items: baseline; padding: 10px 8px; border-bottom: 1px solid var(--border); }85+.rv { position: relative; display: grid; grid-template-columns: auto 1fr auto; column-gap: var(--space-3); row-gap: var(--space-1); align-items: baseline; padding: var(--space-3) var(--space-2); border-bottom: var(--hairline) solid var(--border); }8486 .rv:hover, .rv.sel { background: var(--hover); }8587 .rv.sel { box-shadow: inset 2px 0 0 var(--accent); }8688 .rv > .status { grid-row: 1; }87-.rv-title { min-width: 0; font-weight: 600; overflow-wrap: anywhere; }89+.rv-title { min-width: 0; font-weight: var(--fw-strong); overflow-wrap: anywhere; }90+.rv-title a { color: var(--fg); }91+.rv-title a:hover { color: var(--deep); }8892 .rv-title a::after { content: ''; position: absolute; inset: 0; }89-.rv-side { display: flex; gap: 12px; align-items: baseline; justify-content: flex-end; white-space: nowrap; font: 12.5px var(--mono); }90-.rv-side .desc { font-family: var(--sans); font-size: 13px; }91-.rv-meta { grid-column: 2 / 4; display: flex; flex-wrap: wrap; column-gap: 14px; row-gap: 2px; font: 12px/1.5 var(--mono); color: var(--muted); }92-.rv-meta time { font-size: 1em; }93+.rv-side { display: flex; gap: var(--space-3); align-items: baseline; justify-content: flex-end; white-space: nowrap; font: var(--fw-regular) var(--text-xs) var(--mono); }94+.rv-side .desc { font-family: var(--sans); font-size: var(--text-sm); }95+.rv-meta { grid-column: 2 / 4; display: flex; flex-wrap: wrap; column-gap: var(--space-4); row-gap: var(--space-half); font: var(--fw-regular) var(--text-xs)/1.5 var(--mono); color: var(--muted); }96+.rv-meta time, .rv-meta .mono { font-size: inherit; }9397 .rv-meta .num { font-variant-numeric: tabular-nums; }94-.rv-meta .add { color: var(--pass); }95-.rv-meta .del { color: var(--fail); }98+.rv-meta .add { color: var(--add-fg); }99+.rv-meta .del { color: var(--del-fg); }96100 .rv-meta a, .rv-side a { position: relative; z-index: 1; }97-.rv.layer { margin-left: calc(var(--depth) * 22px); border-left: 1px solid var(--border); }98-.appr { white-space: nowrap; font-weight: 600; color: var(--pass); }99-.appr + .appr { margin-left: 8px; }100-.appr.stale { color: var(--warn); font-weight: normal; }101+.rv.layer { margin-left: calc(var(--depth) * var(--space-6)); border-left: var(--hairline) solid var(--border); }102+.appr { white-space: nowrap; font-weight: var(--fw-strong); color: var(--pass); }103+.appr + .appr { margin-left: var(--space-2); }104+.appr.stale { color: var(--warn); font-weight: var(--fw-regular); }101105 @media (max-width: 720px) {102106 .rv { grid-template-columns: auto 1fr; }103107 .rv-side { grid-column: 2; justify-content: flex-start; }104108 .rv-meta { grid-column: 1 / 3; }105- .rv.layer { margin-left: calc(var(--depth) * 12px); }109+ .rv.layer { margin-left: calc(var(--depth) * var(--space-3)); }106110 }107111 {% endcss %}108112 {% set last = r.revisions[r.revisions.length - 1] %}
@@ -125,54 +129,65 @@
125129 {% macro files(entries, parentHref) %}126130 <table class="list files nav">127131 <tbody>128-{% if parentHref %}<tr><td class="icon">&#8617;</td><td class="main"><a href="{{ parentHref }}" data-key="u">..</a></td><td></td></tr>{% endif %}132+{% if parentHref %}<tr><td class="icon">{{ ui.icon('up') }}</td><td class="main"><a href="{{ parentHref }}" data-key="u">..</a></td><td></td></tr>{% endif %}129133 {% for e in entries %}130-<tr><td class="icon">{{ '&#128193;' | safe if e.type == 'tree' else ('&#128279;' | safe if e.type == 'commit' else '&#128196;' | safe) }}</td><td class="main">{% if e.href %}<a href="{{ e.href }}">{{ e.name }}</a>{% else %}{{ e.name }} <span class="desc">@ {{ e.oid | short }}</span>{% endif %}</td><td class="num size">{{ e.size | size if e.type == 'blob' else '' }}</td></tr>134+<tr><td class="icon">{{ ui.icon('folder' if e.type == 'tree' else ('link' if e.type == 'commit' else 'file')) }}</td><td class="main">{% if e.href %}<a href="{{ e.href }}">{{ e.name }}</a>{% else %}{{ e.name }} <span class="desc">@ {{ e.oid | short }}</span>{% endif %}</td><td class="num size">{{ e.size | size if e.type == 'blob' else '' }}</td></tr>131135 {% endfor %}132136 </tbody>133137 </table>134138 {% endmacro %}135139 136-{# The index and owner pages: repos as rows on shelves by kind (the shelves137- filter), with links (.chip) that jump to a shelf. ui.ts makes them filters and138- adds "has a site" and the sort; keys.ts walks the cards with j and k. #}140+{# The index and owner pages: repos as rows on shelves by kind. Tabs above141+ them name the shelves, "all" first: without JS they jump to a shelf,142+ with it (ui.ts) one shows that shelf alone. Beside them, how to look at143+ the list, which needs JS and shows with it: the globe keeps only repos144+ with a site, and the sort is newest first or by name. keys.ts walks the145+ rows with j and k. #}139146 {% macro shelves(repos, owners = false) %}140147 {% css %}141-.shelf-bar { display: flex; flex-wrap: wrap; gap: 4px 18px; align-items: baseline; margin: 0 0 4px; }148+.shelf-bar { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-6); align-items: center; margin: 0 0 var(--space-2); border-bottom: var(--hairline) solid var(--border); }149+.shelf-bar nav.tabs { flex: 0 1 auto; min-width: 0; }150+.shelf-bar.plain { border-bottom: 0; margin-bottom: calc(-1 * var(--control-h) - var(--space-2)); }142151 .shelf-bar .grow { flex: 1; }143-.chip { font: inherit; font-size: 0.9em; letter-spacing: normal; text-transform: none; color: var(--link); background: none; border: 0; padding: 4px 0; cursor: pointer; white-space: nowrap; }144-.chip:hover { color: var(--deep); background: none; text-decoration: none; }145-.chip[aria-pressed="true"] { color: var(--fg); font-weight: 600; box-shadow: inset 0 -2px 0 var(--accent); }146-.chip .n { font: 11px var(--mono); color: var(--faint); margin-left: 5px; font-variant-numeric: tabular-nums; }147-.shelf-head { display: flex; align-items: baseline; gap: 8px; margin: 26px 0 0; padding-bottom: 6px; border-bottom: 1px solid var(--border); scroll-margin-top: 12px; }152+.shelf-tools { display: flex; gap: var(--space-2); align-items: center; padding: var(--space-1) 0; }153+.shelf-tools[hidden] { display: none; }154+.shelf-head { display: flex; align-items: baseline; gap: var(--space-2); margin: var(--space-6) 0 0; padding-bottom: var(--space-2); border-bottom: var(--hairline) solid var(--border); scroll-margin-top: var(--space-3); }148155 .shelf-head h2 { margin: 0; padding: 0; border: 0; }149-.shelf-head .n { font: 600 11px var(--mono); color: var(--faint); }156+.shelf-head .n { font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--faint); }150157 /* Repos as rows between hairlines: name, what it is, then its sites, reviews and last push. */151158 .cards { list-style: none; margin: 0; padding: 0; }152-.card { position: relative; display: grid; grid-template-columns: minmax(8em, 15em) 1fr auto; column-gap: 16px; align-items: baseline; min-width: 0; padding: 9px 8px; border-bottom: 1px solid var(--border); }159+.card { position: relative; display: grid; grid-template-columns: minmax(8em, 15em) 1fr auto; column-gap: var(--space-4); align-items: baseline; min-width: 0; padding: var(--space-2); border-bottom: var(--hairline) solid var(--border); }153160 .card:hover, .card:focus-within, .card.sel { background: var(--hover); }154161 .card.sel { box-shadow: inset 2px 0 0 var(--accent); }155162 .card-top { min-width: 0; }156-.card-name { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 650; }163+.card-name { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--fw-strong); color: var(--fg); }164+.card-name:hover { color: var(--deep); }157165 .card-name::after { content: ''; position: absolute; inset: 0; }158-.card-name .desc { font-weight: normal; }159-.card-desc { grid-column: 2; margin: 0; color: var(--muted); font-size: 0.92em; overflow-wrap: anywhere; }160-.card-foot { grid-column: 3; display: flex; align-items: baseline; gap: 14px; font: 12px var(--mono); color: var(--faint); white-space: nowrap; }166+.card-name .desc { font-weight: var(--fw-regular); }167+.card-desc { grid-column: 2; margin: 0; color: var(--muted); font-size: var(--text-sm); overflow-wrap: anywhere; }168+.card-foot { grid-column: 3; display: flex; align-items: baseline; gap: var(--space-4); font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--faint); white-space: nowrap; }161169 .card-foot a { position: relative; z-index: 1; }170+.card-foot .site { display: inline-flex; align-items: center; gap: var(--space-1); align-self: center; }162171 .card-foot .grow { display: none; }163-.card-foot time { font-size: 1em; }172+.card-foot time { font-size: inherit; }164173 @media (max-width: 720px) {165- .card { grid-template-columns: 1fr; row-gap: 3px; }174+ .card { grid-template-columns: 1fr; row-gap: var(--space-1); }166175 .card-desc, .card-foot { grid-column: 1; }167176 }168-.shelves-empty { color: var(--muted); padding: 24px 0; text-align: center; }177+.shelves-empty { color: var(--muted); padding: var(--space-6) 0; text-align: center; }169178 {% endcss %}170179 {% set list = repos | shelves %}171-<div class="shelf-bar" data-shelves>172-{% if list.length > 1 %}{% for sh in list %}<a class="chip" href="#{{ sh.id }}" data-shelf="{{ sh.id }}">{{ sh.title }}<span class="n">{{ sh.repos.length }}</span></a>{% endfor %}{% endif %}173-<button type="button" class="chip" data-has-site hidden aria-pressed="false">has a site</button>180+<div class="shelf-bar{% if list.length < 2 %} plain{% endif %}" id="shelves" data-shelves>181+{% if list.length > 1 %}182+{% set tabs = [{href: '#shelves', text: 'all', n: repos.length, cur: true, attrs: 'data-shelf=""'}] %}183+{% for sh in list %}{% set _ = tabs.push({href: '#' + sh.id, text: sh.title, n: sh.repos.length, attrs: 'data-shelf="' + sh.id + '"'}) %}{% endfor %}184+{{ ui.tabs('shelves', tabs) }}185+{% endif %}174186 <span class="grow"></span>175-<button type="button" class="chip" data-sort hidden title="Sort by name or by last push">newest first</button>187+<span class="shelf-tools" hidden>188+{{ ui.iconToggle('globe', 'only repos with a site', false, 'data-has-site') }}189+{{ ui.segmented('sort', [{icon: 'clock', label: 'newest first', pressed: true, attrs: 'data-sort="date"'}, {icon: 'az', label: 'by name, a to z', attrs: 'data-sort="name"'}]) }}190+</span>176191 </div>177192 {% for sh in list %}178193 <section class="shelf" data-shelf="{{ sh.id }}">
@@ -192,6 +207,6 @@
192207 {% endmacro %}193208 194209 {% macro sitesLink(s) %}195-{% if s.counts.sites == 1 and s.sites.length %}<a href="{{ s.sites[0].url }}">{{ s.sites[0].name }}</a>196-{% elif s.counts.sites %}<a href="{{ s.urls.sites.href }}">sites</a>{% endif %}210+{% if s.counts.sites == 1 and s.sites.length %}<a class="site" href="{{ s.sites[0].url }}" title="its site">{{ ui.icon('globe') }}{{ s.sites[0].name }}</a>211+{% elif s.counts.sites %}<a class="site" href="{{ s.urls.sites.href }}" title="its sites">{{ ui.icon('globe') }}sites</a>{% endif %}197212 {% endmacro %}
M site/_includes/private.njk +5 -4
@@ -3,26 +3,27 @@
33 Data: forge, p (title), user, csrf, signal (JSON or ''), repos (owner,44 name, description, lastCommit, href, built). -#}55 {% extends "base.njk" %}6+{% import "ui.njk" as ui %}67 {% block scripts %}78 <script src="{{ assets.webauthn }}" defer></script>89 <script src="{{ assets.login }}" defer></script>910 {% endblock %}1011 {% block head %}<meta name="robots" content="noindex">1112 {% endblock %}12-{% block heading %}<b>Private repos</b>13+{% block heading %}<b>private repos</b>1314 {% endblock %}1415 {% block content %}1516 {% css %}16-.signed-in { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; margin: 0 0 12px; color: var(--muted); font-size: 0.92em; }17+.signed-in { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); align-items: center; margin: 0 0 var(--space-3); color: var(--muted); font-size: var(--text-sm); }1718 .signed-in form { display: inline; }1819 {% endcss %}1920 <div class="signed-in" id="login"{% if signal %} data-signal="{{ signal }}"{% endif %}>2021 <span>Signed in as <b>{{ user }}</b>. These are the private repos you can read; only their readers see them.</span>21-<form method="post" action="/login/logout"><input type="hidden" name="csrf" value="{{ csrf }}"><button type="submit" id="signout">Sign out</button></form>22+<form method="post" action="/login/logout"><input type="hidden" name="csrf" value="{{ csrf }}">{{ ui.button('sign out', false, 'id="signout"', 'submit') }}</form>2223 </div>2324 <div class="box">2425 <table class="list stack nav">25-<thead><tr><th>Repository</th><th>Description</th><th>Updated</th></tr></thead>26+<thead><tr><th>repository</th><th>description</th><th>updated</th></tr></thead>2627 <tbody>2728 {% for s in repos %}2829 <tr><td class="nowrap">{% if s.built %}<a href="{{ s.href }}">{% endif %}<span class="desc">{{ s.owner }} /</span> <b>{{ s.name }}</b>{% if s.built %}</a>{% endif %}</td><td class="main">{{ s.description }}{% if not s.built %} <span class="desc">(no pages yet)</span>{% endif %}</td><td class="date">{{ s.lastCommit | date }}</td></tr>
M site/_includes/search.njk +23 -22
@@ -4,34 +4,35 @@
44 someone searches file contents. -#}55 {% css %}66 /* search: the header button, and the overlay it and "/" open */7-.search-open { display: inline-flex; align-items: center; gap: 6px; flex: none; padding: 4px 9px; }7+.search-open { flex: none; gap: var(--space-2); color: var(--muted); }88 .search-open[hidden] { display: none; }9-.search-open kbd { line-height: 1.3; color: inherit; border-color: currentColor; background: none; }10-#search { position: fixed; inset: 0; z-index: 20; background: rgba(0, 0, 0, 0.3); }11-#search .search-box { position: absolute; top: max(8px, 8vh); left: 50%; transform: translateX(-50%); width: min(680px, calc(100vw - 16px)); background: var(--raised); border: 1px solid var(--border); padding: 8px; box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2); }12-#search input { width: 100%; font: 16px var(--sans); padding: 8px 10px; border: 1px solid var(--border); background: var(--bg); color: var(--fg); }13-#search input:focus, #finder input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }14-#search ol { list-style: none; margin: 4px 0 0; padding: 0; max-height: 65vh; overflow: auto; }15-#search li.group { margin: 0 0 2px; padding: 12px 8px 4px; font: 600 10.5px/1.4 var(--mono); color: var(--faint); text-transform: uppercase; letter-spacing: 0.08em; border-bottom: 1px solid var(--border); }16-#search li.group span { font-weight: normal; margin-left: 4px; }17-#search li a { display: block; padding: 5px 8px; color: var(--fg); }9+.search-open kbd { border: 0; padding: 0; background: none; color: var(--faint); }10+#search { position: fixed; inset: 0; z-index: 20; background: var(--scrim); }11+#search .search-box { position: absolute; top: max(8px, 8vh); left: 50%; transform: translateX(-50%); width: min(680px, calc(100vw - 16px)); background: var(--raised); border: var(--hairline) solid var(--border); padding: var(--space-2); box-shadow: var(--shadow); }12+#search input { width: 100%; font: var(--fw-regular) var(--text-lg) var(--sans); padding: var(--space-2) var(--space-3); border: var(--hairline) solid var(--border-strong); background: var(--bg); color: var(--fg); }13+#search input:focus, #finder input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 var(--hairline) var(--accent); }14+#search ol { list-style: none; margin: var(--space-1) 0 0; padding: 0; max-height: 65vh; overflow: auto; }15+#search li.group { margin: 0 0 var(--space-half); padding: var(--space-3) var(--space-2) var(--space-1); font-size: var(--text-sm); font-weight: var(--fw-strong); color: var(--muted); border-bottom: var(--hairline) solid var(--border); }16+#search li.group span { font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--faint); margin-left: var(--space-1); }17+#search li a { display: block; padding: var(--space-1) var(--space-2); color: var(--fg); }1818 #search li a:hover { background: var(--hover); text-decoration: none; }1919 #search li.sel a, #search li.sel button { background: var(--hover); box-shadow: inset 2px 0 0 var(--accent); }2020 #search .t, #search .m { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }21-#search .t.mono { font: 0.9em var(--mono); }22-#search .m.mono { font-family: var(--mono); white-space: pre; }23-#search .m { font-size: 0.82em; color: var(--muted); }24-#search b { color: var(--link); font-weight: 600; }25-#search li.more button { border: 0; background: none; padding: 5px 8px; width: 100%; text-align: left; }26-#search li.note { padding: 5px 8px; font-size: 0.85em; color: var(--muted); }27-#search .hint { margin: 8px 8px 2px; font-size: 0.85em; color: var(--muted); }21+#search .t.mono { font-size: var(--text-sm); }22+#search .m.mono { white-space: pre; }23+#search .m { font-size: var(--text-xs); color: var(--muted); }24+#search b { color: var(--link); font-weight: var(--fw-strong); }25+#search li.more button { min-height: 0; border: 0; background: none; padding: var(--space-1) var(--space-2); width: 100%; justify-content: flex-start; color: var(--link); }26+#search li.note { padding: var(--space-1) var(--space-2); font-size: var(--text-sm); color: var(--muted); }27+#search .hint { margin: var(--space-2) var(--space-2) var(--space-half); font-size: var(--text-sm); color: var(--muted); }2828 #search .hint:empty { display: none; }2929 /* file finder */30-#finder { position: fixed; top: 10%; left: 50%; transform: translateX(-50%); z-index: 20; width: min(640px, 94vw); background: var(--raised); border: 1px solid var(--border); padding: 8px; box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2); }31-#finder input { width: 100%; font: 15px var(--mono); padding: 8px; border: 1px solid var(--border); background: var(--bg); color: var(--fg); }32-#finder ol { list-style: none; margin: 6px 0 0; padding: 0; max-height: 60vh; overflow: auto; }33-#finder li a { display: block; padding: 4px 8px; font-family: var(--mono); font-size: 0.9em; color: var(--fg); }30+#finder { position: fixed; top: 10%; left: 50%; transform: translateX(-50%); z-index: 20; width: min(640px, 94vw); background: var(--raised); border: var(--hairline) solid var(--border); padding: var(--space-2); box-shadow: var(--shadow); }31+#finder input { width: 100%; font: var(--fw-regular) var(--text-md) var(--mono); padding: var(--space-2); border: var(--hairline) solid var(--border-strong); background: var(--bg); color: var(--fg); }32+#finder ol { list-style: none; margin: var(--space-2) 0 0; padding: 0; max-height: 60vh; overflow: auto; }33+#finder li a { display: block; padding: var(--space-1) var(--space-2); font-family: var(--mono); font-size: var(--text-sm); color: var(--fg); }3434 #finder li.sel a { background: var(--hover); box-shadow: inset 2px 0 0 var(--accent); }3535 #finder li b { color: var(--link); }3636 {% endcss %}37-<button type="button" class="search-open" hidden data-search="{{ r.urls.search.href if r else '/repos.json' }}"{% if o and o.owner %} data-owner="{{ o.owner }}"{% endif %}{% if r %} data-code="{{ assets.codesearch }}" data-sqlite="{{ assets.sqlite3 }}" data-wasm="{{ assets.sqlite3wasm }}"{% endif %} title="Search (/)" aria-label="Search"><svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true"><circle cx="6.8" cy="6.8" r="4.6" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M10.3 10.3 14 14" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg><kbd class="hide-sm">/</kbd></button>37+{% import "ui.njk" as ui %}38+<button type="button" class="search-open" hidden data-search="{{ r.urls.search.href if r else '/repos.json' }}"{% if o and o.owner %} data-owner="{{ o.owner }}"{% endif %}{% if r %} data-code="{{ assets.codesearch }}" data-sqlite="{{ assets.sqlite3 }}" data-wasm="{{ assets.sqlite3wasm }}"{% endif %} title="search (/)" aria-label="search">{{ ui.icon('search') }}<kbd class="hide-sm">/</kbd></button>
A site/_includes/ui.njk +93 -0
@@ -0,0 +1,93 @@
1+{#- The forge's controls, one macro each; their CSS is in style.css under2+ "controls". Pages use these rather than writing the markup again, so a3+ control looks and behaves the same everywhere; /design/ shows each one.4+ Import it with: {% import "ui.njk" as ui %}5+6+ The rule: tabs say where you are; a checkbox turns one thing on or off;7+ a segmented control picks one of a few; an icon toggle is a checkbox8+ drawn as a symbol (with aria-pressed, a label and a title); buttons do9+ things; links only go places. Chrome is written in lowercase here and10+ in templates; nothing is case-transformed by CSS.11+12+ attrs, where a macro takes it, is extra attributes as written, e.g.13+ 'data-wrap' or 'id="signin"'. -#}14+15+{# A 16px line icon in the text's colour. With a label it is an image with16+ that name; without, decoration beside words that say the same. #}17+{% macro icon(name, label = '') -%}18+<svg class="icon" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"{% if label %} role="img" aria-label="{{ label }}"{% else %} aria-hidden="true"{% endif %}>19+{%- if name == 'globe' %}<circle cx="8" cy="8" r="6.3"/><ellipse cx="8" cy="8" rx="2.7" ry="6.3"/><path d="M1.9 6h12.2M1.9 10h12.2"/>20+{%- elif name == 'clock' %}<circle cx="8" cy="8" r="6.3"/><path d="M8 4.4V8l2.6 1.6"/>21+{%- elif name == 'az' %}<path d="M1.8 13 4.6 3.5 7.4 13M2.8 10h3.6M9.4 3.5h4.4l-4.4 9.5h4.6"/>22+{%- elif name == 'folder' %}<path d="M1.8 4.2c0-.6.4-1 1-1h3.4l1.5 1.7h5.5c.6 0 1 .4 1 1v6.9c0 .6-.4 1-1 1H2.8c-.6 0-1-.4-1-1z"/>23+{%- elif name == 'file' %}<path d="M3.5 1.8h5.6l3.4 3.4v9H3.5zM9 1.8v3.5h3.5"/>24+{%- elif name == 'link' %}<path d="M6.7 9.3a2.8 2.8 0 0 0 4 0l2.1-2.1a2.8 2.8 0 0 0-4-4l-.8.8M9.3 6.7a2.8 2.8 0 0 0-4 0L3.2 8.8a2.8 2.8 0 0 0 4 4l.8-.8"/>25+{%- elif name == 'up' %}<path d="M6.5 3.2 3 6.7l3.5 3.5M3.2 6.7h6.3a3.5 3.5 0 0 1 0 7H8"/>26+{%- elif name == 'branch' %}<circle cx="4.5" cy="3.5" r="1.6"/><circle cx="4.5" cy="12.5" r="1.6"/><circle cx="11.5" cy="5" r="1.6"/><path d="M4.5 5.1v5.8M11.5 6.6c0 2.6-2.2 3.4-5.4 4.3"/>27+{%- elif name == 'search' %}<circle cx="6.8" cy="6.8" r="4.6"/><path d="M10.3 10.3 14 14"/>28+{%- endif %}</svg>29+{%- endmacro %}30+31+{# A button. primary: the one main action of its place, filled with the32+ accent; otherwise secondary. #}33+{% macro button(text, primary = false, attrs = '', type = 'button') -%}34+<button type="{{ type }}"{% if primary %} class="primary"{% endif %}{% if attrs %} {{ attrs | safe }}{% endif %}>{{ text }}</button>35+{%- endmacro %}36+37+{# An icon toggle: on or off, drawn as a symbol; filled with ink when on.38+ label names what it does when on; it is the button's name and title. #}39+{% macro iconToggle(name, label, pressed = false, attrs = '') -%}40+<button type="button" class="icon-btn" aria-pressed="{{ 'true' if pressed else 'false' }}" aria-label="{{ label }}" title="{{ label }}"{% if attrs %} {{ attrs | safe }}{% endif %}>{{ icon(name) }}</button>41+{%- endmacro %}42+43+{# A segmented control: one of a few, as joined buttons, the chosen one44+ filled with ink. items: {label, icon (optional: the label becomes the45+ name and title), pressed, attrs}. #}46+{% macro segmented(label, items, attrs = '') -%}47+<span class="segmented" role="group" aria-label="{{ label }}"{% if attrs %} {{ attrs | safe }}{% endif %}>48+{%- for it in items %}<button type="button"{% if it.icon %} class="icon-btn" aria-label="{{ it.label }}" title="{{ it.label }}"{% endif %} aria-pressed="{{ 'true' if it.pressed else 'false' }}"{% if it.attrs %} {{ it.attrs | safe }}{% endif %}>{{ icon(it.icon) if it.icon else it.label }}</button>{% endfor -%}49+</span>50+{%- endmacro %}51+52+{# A checkbox with its words: on or off. #}53+{% macro checkbox(text, checked = false, attrs = '') -%}54+<label class="check"><input type="checkbox"{% if checked %} checked{% endif %}{% if attrs %} {{ attrs | safe }}{% endif %}>{{ text }}</label>55+{%- endmacro %}56+57+{# Tabs: where you are. items: {href, text, cur, key (a keys.ts shortcut),58+ n (a count after the words), off (leave it out), attrs}. #}59+{% macro tabs(label, items, cls = '', attrs = '') -%}60+<nav class="tabs{% if cls %} {{ cls }}{% endif %}" aria-label="{{ label }}"{% if attrs %} {{ attrs | safe }}{% endif %}>61+{% for t in items %}{% if not t.off %}<a href="{{ t.href }}"{% if t.key %} data-key="{{ t.key }}"{% endif %}{% if t.cur %} class="cur" aria-current="page"{% endif %}{% if t.attrs %} {{ t.attrs | safe }}{% endif %}>{{ t.text }}{% if t.n != null %}<span class="n">{{ t.n }}</span>{% endif %}</a>62+{% endif %}{% endfor -%}63+</nav>64+{%- endmacro %}65+66+{# A text box. mono: for what is copied, like a URL or a command. #}67+{% macro input(label, value = '', attrs = '', mono = false) -%}68+<input class="input{% if mono %} mono{% endif %}" aria-label="{{ label }}"{% if value %} value="{{ value }}"{% endif %}{% if attrs %} {{ attrs | safe }}{% endif %}>69+{%- endmacro %}70+71+{% macro textarea(label, placeholder = '', attrs = '') -%}72+<textarea class="input" aria-label="{{ label }}"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if attrs %} {{ attrs | safe }}{% endif %}></textarea>73+{%- endmacro %}74+75+{# A list: a table with rows between hairlines that restacks on phones76+ (stack). heads: column names, or {text, cls, title}. The rows are the77+ call's body: {% call ui.listTable(['branch', 'updated']) %}<tr>...{% endcall %} #}78+{% macro listTable(heads = [], cls = 'stack nav', attrs = '') -%}79+<table class="list{% if cls %} {{ cls }}{% endif %}"{% if attrs %} {{ attrs | safe }}{% endif %}>80+{% if heads.length %}<thead><tr>{% for h in heads %}<th{% if h.cls %} class="{{ h.cls }}"{% endif %}{% if h.title %} title="{{ h.title }}"{% endif %}>{{ h.text if h.text != null else h }}</th>{% endfor %}</tr></thead>81+{% endif %}<tbody>82+{{ caller() }}</tbody>83+</table>84+{%- endmacro %}85+86+{# A review's status: the one filled label, white on its colour. #}87+{% macro status(s) %}<span class="status {{ s }}">{{ s }}</span>{% endmacro %}88+89+{# A section heading on a rule: its words, then a count (n) and a quieter90+ note (desc). #}91+{% macro heading(text, n = null, desc = '', id = '') -%}92+<h2{% if id %} id="{{ id }}"{% endif %}>{{ text }}{% if n != null %} <span class="n">{{ n }}</span>{% endif %}{% if desc %} <span class="desc">{{ desc }}</span>{% endif %}</h2>93+{%- endmacro %}
M site/assets/diffs.ts +5 -5
M site/assets/discuss.ts +6 -6
@@ -112,11 +112,11 @@
112112 const box = document.createElement('div');113113 box.className = 'cmts';114114 box.dataset.lineForm = where;115- box.innerHTML = '<form class="linecmt"><div class="where"></div><textarea name="body" maxlength="32768" placeholder="Comment on this line. Markdown is fine."></textarea>' +116- '<div class="reply-bar"><span class="reply-status" role="status"></span><span class="grow"></span><button type="button" data-cancel>Cancel</button><button type="submit" class="go">Comment</button></div></form>';115+ box.innerHTML = '<form class="linecmt"><div class="where"></div><textarea class="input" name="body" maxlength="32768" placeholder="Comment on this line. Markdown is fine."></textarea>' +116+ '<div class="reply-bar"><span class="reply-status" role="status"></span><span class="grow"></span><button type="button" data-cancel>cancel</button><button type="submit" class="primary">comment</button></div></form>';117117 box.querySelector('.where')!.textContent = `${where}, as ${me.user}`;118118 const text = box.querySelector('textarea')!;119- text.setAttribute('aria-label', `Comment on ${where}`);119+ text.setAttribute('aria-label', `comment on ${where}`);120120 text.value = draft(`line ${where}`);121121 below(row, box);122122 if (focus) text.focus();
@@ -202,8 +202,8 @@
202202 if (approve && me.user === form.dataset.author) approve.hidden = true;203203 else if (approve && shown({ kind: 'approve' })) {204204 approve.disabled = true;205- approve.textContent = 'Approved';206- approve.title = 'You approved this tree';205+ approve.textContent = 'approved';206+ approve.title = 'you approved this tree';207207 }208208 }209209 const hint = lines();
@@ -250,7 +250,7 @@
250250 watch();251251 }252252 busy(form, false);253- b.textContent = 'Approved';253+ b.textContent = 'approved';254254 b.disabled = true;255255 } catch (err) {256256 say(form, failed(err), true);
M site/assets/insights.ts +11 -11
@@ -368,11 +368,11 @@
368368 box.appendChild(k);369369 };370370 const n = (k: string): number => t[k] ?? 0;371- tile('Pushes', count(n('pushes')), d.from ? `since ${dayLong(d.from)}` : 'none yet');372- tile('Builds', count(n('builds')), t.build_ms_median != null ? `median ${secs(t.build_ms_median / 1000)}` : 'none yet');373- tile('Reviews landed', `${count(n('landed'))} of ${count(n('reviews'))}`, t.land_hours_median != null ? `median ${hours(t.land_hours_median)} to land` : `${count(n('closed'))} closed`);374- tile('Revisions', t.revisions_median != null ? r1(t.revisions_median) : '-', 'per review, median');375- tile('CI passed', n('ci') ? `${Math.round((100 * n('ci_pass')) / n('ci'))}%` : '-', n('ci') ? `${n('ci_pass')} of ${plural(n('ci'), 'run')}${t.ci_seconds_median != null ? `, median ${secs(t.ci_seconds_median)}` : ''}` : 'no attestations yet');371+ tile('pushes', count(n('pushes')), d.from ? `since ${dayLong(d.from)}` : 'none yet');372+ tile('builds', count(n('builds')), t.build_ms_median != null ? `median ${secs(t.build_ms_median / 1000)}` : 'none yet');373+ tile('reviews landed', `${count(n('landed'))} of ${count(n('reviews'))}`, t.land_hours_median != null ? `median ${hours(t.land_hours_median)} to land` : `${count(n('closed'))} closed`);374+ tile('revisions', t.revisions_median != null ? r1(t.revisions_median) : '-', 'per review, median');375+ tile('ci passed', n('ci') ? `${Math.round((100 * n('ci_pass')) / n('ci'))}%` : '-', n('ci') ? `${n('ci_pass')} of ${plural(n('ci'), 'run')}${t.ci_seconds_median != null ? `, median ${secs(t.ci_seconds_median)}` : ''}` : 'no attestations yet');376376 }377377 378378 function draw(d: Data): void {
@@ -383,7 +383,7 @@
383383 chart('builds', (fig, plot) => {384384 if (!d.builds.length) return empty(fig, 'No builds yet.');385385 dots(plot, d.builds.map(([t, ms]) => ({ t, v: ms / 1000, head: when(t), rows: [[secs(ms / 1000), 'to build']] })), (v) => secs(v));386- table(fig, ['Built', 'Took'], () => [...d.builds].reverse().map(([t, ms]) => [when(t), secs(ms / 1000)]));386+ table(fig, ['built', 'took'], () => [...d.builds].reverse().map(([t, ms]) => [when(t), secs(ms / 1000)]));387387 });388388 389389 chart('pushes', (fig, plot) => {
@@ -395,7 +395,7 @@
395395 for (const s of fig.querySelectorAll('.ins-bucket')) s.textContent = bk.unit;396396 const every = Math.ceil(bk.keys.length / Math.max(2, Math.floor((plot.clientWidth || 440) / 70)));397397 columns(plot, bk.keys.map((k) => ({ label: bucketLabel(bk.unit, k), head: `${bk.unit === 'week' ? 'week of ' : ''}${bk.unit === 'month' ? bucketLabel('month', k) : dayLong(k)}`, segs: [{ v: sums.get(k) ?? 0, cls: 's1', name: 'pushes' }] })), (v) => count(v), every);398- table(fig, [bk.unit === 'week' ? 'Week of' : bk.unit === 'month' ? 'Month' : 'Day', 'Pushes'], () => bk.keys.filter((k) => sums.get(k)).reverse().map((k) => [dayLong(k), String(sums.get(k))]));398+ table(fig, [bk.unit === 'week' ? 'week of' : bk.unit === 'month' ? 'month' : 'day', 'pushes'], () => bk.keys.filter((k) => sums.get(k)).reverse().map((k) => [dayLong(k), String(sums.get(k))]));399399 });400400 401401 chart('land', (fig, plot) => {
@@ -403,7 +403,7 @@
403403 if (!landed.length) return empty(fig, 'No review has landed yet.');404404 const h = (r: ReviewRow): number => Math.max(0, (r[3]! - r[2]!) / 3600e3);405405 dots(plot, landed.map((r) => ({ t: r[3]!, v: h(r), head: `${reviewName(r)}, landed ${when(r[3]!)}`, rows: [[hours(h(r)), 'to land'], [String(r[5]), r[5] === 1 ? 'revision' : 'revisions']] })), (v) => hours(v));406- table(fig, ['Review', 'Landed', 'Took', 'Revisions'], () => [...landed].reverse().map((r) => [reviewName(r), when(r[3]!), hours(h(r)), String(r[5])]));406+ table(fig, ['review', 'landed', 'took', 'revisions'], () => [...landed].reverse().map((r) => [reviewName(r), when(r[3]!), hours(h(r)), String(r[5])]));407407 });408408 409409 chart('revisions', (fig, plot) => {
@@ -415,7 +415,7 @@
415415 for (const r of revs) n[Math.min(r[5], cap)]++;416416 const name = (i: number): string => (i === cap && top > cap ? `${i}+` : String(i));417417 columns(plot, n.slice(1).map((v, i) => ({ label: name(i + 1), head: `${name(i + 1)} ${i === 0 ? 'revision' : 'revisions'}`, segs: [{ v, cls: 's1', name: v === 1 ? 'review' : 'reviews' }] })), (v) => count(v));418- table(fig, ['Review', 'Revisions', 'Approvals', 'Ended'], () => [...revs].reverse().map((r) => [reviewName(r), String(r[5]), String(r[6]), r[4] ?? 'open']));418+ table(fig, ['review', 'revisions', 'approvals', 'ended'], () => [...revs].reverse().map((r) => [reviewName(r), String(r[5]), String(r[6]), r[4] ?? 'open']));419419 });420420 421421 chart('ci', (fig, plot) => {
@@ -428,7 +428,7 @@
428428 const every = Math.ceil(bk.keys.length / Math.max(2, Math.floor((plot.clientWidth || 440) / 70)));429429 columns(plot, bk.keys.map((k) => ({ label: bucketLabel(bk.unit, k), head: `${bk.unit === 'week' ? 'week of ' : ''}${dayLong(k)}`, segs: [{ v: pass.get(k) ?? 0, cls: 's1', name: 'passed' }, { v: fail.get(k) ?? 0, cls: 'fail', name: 'failed' }] })), (v) => count(v), every);430430 if (!fig.querySelector('.legend')) legend(fig, [['s1', 'passed'], ['fail', 'failed']]);431- table(fig, [bk.unit === 'week' ? 'Week of' : bk.unit === 'month' ? 'Month' : 'Day', 'Passed', 'Failed'], () => bk.keys.filter((k) => pass.get(k) || fail.get(k)).reverse().map((k) => [dayLong(k), String(pass.get(k) ?? 0), String(fail.get(k) ?? 0)]));431+ table(fig, [bk.unit === 'week' ? 'week of' : bk.unit === 'month' ? 'month' : 'day', 'passed', 'failed'], () => bk.keys.filter((k) => pass.get(k) || fail.get(k)).reverse().map((k) => [dayLong(k), String(pass.get(k) ?? 0), String(fail.get(k) ?? 0)]));432432 });433433 434434 chart('ciTime', (fig, plot) => {
@@ -436,7 +436,7 @@
436436 if (!runs.length) return empty(fig, 'No CI attestations yet (forge attest).');437437 dots(plot, runs.map(([t, s, p]) => ({ t, v: s!, fail: !p, head: when(t), rows: [[secs(s!), p ? 'passed' : 'failed', p ? 's1' : 'fail']] })), (v) => secs(v));438438 if (!fig.querySelector('.legend')) legend(fig, [['s1', 'passed'], ['fail', 'failed']]);439- table(fig, ['Started', 'Took', 'Result'], () => [...runs].reverse().map(([t, s, p]) => [when(t), secs(s!), p ? 'passed' : 'failed']));439+ table(fig, ['started', 'took', 'result'], () => [...runs].reverse().map(([t, s, p]) => [when(t), secs(s!), p ? 'passed' : 'failed']));440440 });441441 442442 const repos = root.querySelector('#ins-repos tbody');
M site/assets/keys.ts +1 -1
M site/assets/logo.svg +1 -1
M site/assets/search.ts +11 -11
@@ -46,7 +46,7 @@ import { hasAll, marks, narrow, score as match, snippet, terms } from './query.t
4646 inText: boolean;4747 }4848 49- const KINDS = ['Repositories', 'Reviews', 'Commits', 'Wiki', 'Files'];49+ const KINDS = ['repositories', 'reviews', 'commits', 'wiki', 'files'];5050 const PER_KIND = 6;5151 5252 // Code search: lines from the worker, for the query they answer.
@@ -107,21 +107,21 @@ import { hasAll, marks, narrow, score as match, snippet, terms } from './query.t
107107 const owner = button!.dataset.owner;108108 const list: Rec[] = [];109109 for (const [slug, desc, href] of ix.repos ?? []) {110- if (!owner || String(slug).startsWith(owner + '/')) list.push(rec('Repositories', String(href), String(slug), '', String(desc)));110+ if (!owner || String(slug).startsWith(owner + '/')) list.push(rec('repositories', String(href), String(slug), '', String(desc)));111111 }112112 const home = ix.home ?? '/';113113 for (const [id, title, branch, author, status] of ix.reviews ?? []) {114- list.push(rec('Reviews', `${home}reviews/${id}/`, `#${id} ${title}`, `${branch}, ${author ? author + ', ' : ''}${status}`));114+ list.push(rec('reviews', `${home}reviews/${id}/`, `#${id} ${title}`, `${branch}, ${author ? author + ', ' : ''}${status}`));115115 }116116 for (const [sha, subject, author, date, body] of ix.commits ?? []) {117- list.push(rec('Commits', `${home}commit/${sha}/`, String(subject), `${String(sha).slice(0, 7)} ${author}, ${date}`, String(body), String(sha)));117+ list.push(rec('commits', `${home}commit/${sha}/`, String(subject), `${String(sha).slice(0, 7)} ${author}, ${date}`, String(body), String(sha)));118118 }119- for (const [href, title, text] of ix.wiki ?? []) list.push(rec('Wiki', String(href), String(title), '', String(text)));119+ for (const [href, title, text] of ix.wiki ?? []) list.push(rec('wiki', String(href), String(title), '', String(text)));120120 codeUrl = ix.code ?? null;121121 blobBase = ix.blob ?? '';122122 if (ix.files) {123123 const paths = (await (await fetch(ix.files)).text()).split('\n').filter(Boolean);124- for (const p of paths) list.push(rec('Files', `${ix.blob}${enc(p)}/`, p, '', '', '', true));124+ for (const p of paths) list.push(rec('files', `${ix.blob}${enc(p)}/`, p, '', '', '', true));125125 }126126 return list;127127 }
@@ -228,27 +228,27 @@ import { hasAll, marks, narrow, score as match, snippet, terms } from './query.t
228228 const ws = codeWords(q);229229 const fresh = code?.q === q;230230 if (fresh && code!.error) {231- heading('Code', '');231+ heading('code', '');232232 note(code!.error);233233 return true;234234 }235235 const hits = !code ? [] : fresh ? code.hits : code.hits.filter((h) => hasAll(h.text, ws));236236 if (!hits.length) {237237 if (fresh) return false;238- heading('Code', '');238+ heading('code', '');239239 note('Searching file contents');240240 return true;241241 }242242 const more = fresh && code!.more;243- heading('Code', `${hits.length}${more ? '+' : ''}`);244- const shown = expanded.has('Code') ? hits : hits.slice(0, PER_KIND);243+ heading('code', `${hits.length}${more ? '+' : ''}`);244+ const shown = expanded.has('code') ? hits : hits.slice(0, PER_KIND);245245 for (const h of shown) {246246 link(`${blobBase}${enc(h.path)}/${h.source ? 'source/' : ''}#l${h.line}`, [document.createTextNode(`${h.path}:${h.line}`)], true, marked(h.text, ws), true);247247 }248248 const rest = hits.length - shown.length;249249 if (rest || (more && code!.limit < CODE_MORE)) {250250 moreRow(rest ? `${rest}${more ? '+' : ''} more` : 'more', () => {251- expanded.add('Code');251+ expanded.add('code');252252 if (more && code!.limit < CODE_MORE) runCode(q, CODE_MORE);253253 });254254 } else if (more) note('More lines match than are shown here; a longer query narrows them.');
M site/assets/style.css +290 -193
@@ -1,9 +1,17 @@
1-/* forge: the base stylesheet, light and dark, phone first: tokens, layout,2- lists, code views. CSS for one feature lives with its markup, in a3- template or include ({% css %}), and reaches only the pages that use it. */4-/* The look: square corners, content on the page between 1px rules, labels5- in small uppercase monospace, one accent (#3148F6) for links, the current6- tab's bar, focus and primary actions, on a plain page. */1+/* forge: the base stylesheet, light and dark, phone first: tokens, then2+ layout, controls, lists and code views. CSS for one feature lives with3+ its markup, in a template or include ({% css %}), and reaches only the4+ pages that use it. The rules for all of it are in DESIGN.md, and5+ lib/check-css.ts holds every stylesheet to them in CI.6+7+ The look: a plain page, square corners, content between 1px rules rather8+ than in boxes; Open Sans for the interface, Noto Sans Mono for what is9+ copied or compared, Noto Serif for prose people wrote; hierarchy from10+ weight, size and colour, never capitals or tracking; one accent11+ (#3148F6) for links, the current tab's bar, focus and primary actions. */12+13+/* == tokens == Every colour, size, weight and family is here; nothing14+ below this block names one. The style guide (/design/) shows them. */715 /* The fonts, self-hosted, variable in weight, cut to Latin and the symbols816 the pages print (fonts/make-fonts.sh). The sans is preloaded (base.njk). */917 @font-face { font-family: "Open Sans"; src: url(./fonts/open-sans.woff2) format("woff2"); font-weight: 300 800; font-style: normal; font-display: swap; }
@@ -11,189 +19,292 @@
1119 @font-face { font-family: "Noto Serif"; src: url(./fonts/noto-serif.woff2) format("woff2"); font-weight: 400 700; font-style: normal; font-display: swap; }1220 @font-face { font-family: "Noto Serif"; src: url(./fonts/noto-serif-italic.woff2) format("woff2"); font-weight: 400 700; font-style: italic; font-display: swap; }1321 :root {22+ /* colour: the page, surfaces raised over it, ink in three strengths23+ (muted keeps 4.5:1 on the page), rules, and the accent */1424 --bg: #e9e9e9;25+ --raised: #ffffff;26+ --hover: #f4f4f4;1527 --fg: #1c2127;1628 --muted: #5b6169;17- --faint: #8a9097;18- --border: #d2d2d2;19- /* A raised surface over the page, translucent, and the same as one opaque20- colour, for what content scrolls under (sticky headers, code gutters). */21- --subtle: rgba(255, 255, 255, 0.55);22- --raised: #f5f5f5;23- --hover: rgba(255, 255, 255, 0.7);29+ --faint: #7c828a;30+ --border: #d2d4d7;31+ --border-strong: #a9aeb4;2432 --link: #3148f6;2533 --accent: #3148f6;2634 --deep: #1c4b8f;35+ --accent-hover: #1c4b8f;36+ --on-accent: #ffffff;37+ /* meaning: diffs, CI, warnings */2738 --add-bg: rgba(14, 132, 32, 0.1);28- --add-fg: #0e8420;39+ --add-fg: #0b7a1d;2940 --add-ln: rgba(14, 132, 32, 0.2);3041 --del-bg: rgba(199, 22, 43, 0.08);3142 --del-fg: #c7162b;3243 --del-ln: rgba(199, 22, 43, 0.17);3344 --hunk-bg: rgba(49, 72, 246, 0.07);3445 --hunk-fg: #1c4b8f;35- --pass: #0e8420;46+ --pass: #0b7a1d;3647 --fail: #c7162b;37- --warn: #b46a00;48+ --warn: #965800;3849 --merged: #3148f6;50+ /* status labels: white on a fill, the same in both modes */51+ --status-open: #0e8420;52+ --status-merged: #3148f6;53+ --status-closed: #c7162b;54+ --status-other: #5b6169;55+ /* a linked line, floating panels, dialogs' backdrop, masks */56+ --target-bg: rgba(255, 213, 79, 0.25);57+ --target-gut: rgba(255, 213, 79, 0.4);58+ --shadow: 0 8px 24px rgba(0, 0, 0, 0.12);59+ --scrim: rgba(0, 0, 0, 0.3);60+ --mask: #000;61+ /* syntax highlighting (highlight.js classes) */62+ --tok-comment: #59636e;63+ --tok-keyword: #cf222e;64+ --tok-string: #0a3069;65+ --tok-const: #0550ae;66+ --tok-title: #8250df;67+ --tok-builtin: #953800;68+ --tok-tag: #116329;69+ /* charts (insights.ts): checked for colour-blind readers on both pages */70+ --viz-1: #3148f6;71+ --viz-fail: #c7162b;72+ --viz-grid: #d9d9d9;73+ --viz-axis: #a9aeb4;74+75+ /* type: the interface, what is copied or compared, and prose */3976 --sans: "Open Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;4077 --mono: "Noto Sans Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;4178 --serif: "Noto Serif", Georgia, Cambria, "Times New Roman", serif;79+ --text-xs: 12px;80+ --text-code: 12.5px;81+ --text-sm: 13.5px;82+ --text-md: 15px;83+ --text-prose: 16px;84+ --text-lg: 18px;85+ --text-xl: 22px;86+ --text-2xl: 28px;87+ /* mono inline in sans text runs large; this brings it level */88+ --text-inline-mono: 0.9em;89+ --fw-regular: 400;90+ --fw-medium: 500;91+ --fw-strong: 650;92+93+ /* space: a 4px scale */94+ --space-half: 2px;95+ --space-1: 4px;96+ --space-2: 8px;97+ --space-3: 12px;98+ --space-4: 16px;99+ --space-5: 20px;100+ --space-6: 24px;101+ --space-8: 32px;102+ --space-10: 40px;103+ --hairline: 1px;104+ --bar: 3px;105+ --radius: 0;106+ --control-h: 30px;42107 color-scheme: light dark;43108 }44109 @media (prefers-color-scheme: dark) {45110 :root {46- --bg: #1c2127;47- --fg: #e9e9e9;48- --muted: #a3a9b0;49- --faint: #737a82;50- --border: #2f363e;51- --subtle: rgba(255, 255, 255, 0.04);52- --raised: #23282e;53- --hover: rgba(255, 255, 255, 0.06);111+ --bg: #08090b;112+ --raised: #111418;113+ --hover: #15181d;114+ --fg: #e2e4e7;115+ --muted: #8f959d;116+ --faint: #6b7178;117+ --border: #1f2329;118+ --border-strong: #3a4048;54119 --link: #8e9bff;55120 --deep: #b4bcff;56- --add-bg: rgba(79, 191, 95, 0.13);121+ --accent-hover: #2a3ee0;122+ --add-bg: rgba(79, 191, 95, 0.12);57123 --add-fg: #4fbf5f;58- --add-ln: rgba(79, 191, 95, 0.26);59- --del-bg: rgba(240, 96, 110, 0.13);124+ --add-ln: rgba(79, 191, 95, 0.24);125+ --del-bg: rgba(240, 96, 110, 0.12);60126 --del-fg: #f0606e;61- --del-ln: rgba(240, 96, 110, 0.26);62- --hunk-bg: rgba(142, 155, 255, 0.1);127+ --del-ln: rgba(240, 96, 110, 0.24);128+ --hunk-bg: rgba(142, 155, 255, 0.09);63129 --hunk-fg: #8e9bff;64130 --pass: #4fbf5f;65131 --fail: #f0606e;66132 --warn: #e3a33b;67133 --merged: #8e9bff;68- }134+ --target-bg: rgba(255, 213, 79, 0.12);135+ --target-gut: rgba(255, 213, 79, 0.22);136+ --shadow: 0 8px 28px rgba(0, 0, 0, 0.7);137+ --scrim: rgba(0, 0, 0, 0.6);138+ --tok-comment: #8b929b;139+ --tok-keyword: #ff7b72;140+ --tok-string: #a5d6ff;141+ --tok-const: #79c0ff;142+ --tok-title: #d2a8ff;143+ --tok-builtin: #ffa657;144+ --tok-tag: #7ee787;145+ --viz-1: #6f7fff;146+ --viz-fail: #f0606e;147+ --viz-grid: #1a1e23;148+ --viz-axis: #3a4048;149+ }69150 }151+/* == end tokens == */70152 71-*, ::before, ::after { box-sizing: border-box; border-radius: 0; }153+*, ::before, ::after { box-sizing: border-box; border-radius: var(--radius); }72154 html { -webkit-text-size-adjust: 100%; }73-body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.5 var(--sans); }155+/* hidden wins over a display a control's class gives it */156+[hidden] { display: none !important; }157+body { margin: 0; background: var(--bg); color: var(--fg); font: var(--fw-regular) var(--text-md)/1.5 var(--sans); }74158 a { color: var(--link); text-decoration: none; }75159 a:hover { color: var(--deep); text-decoration: underline; }76160 :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }161+b, strong, th { font-weight: var(--fw-strong); }77162 code, kbd, pre, time, .mono, .sha { font-family: var(--mono); }78-time { font-size: 0.92em; }79-.sha { font-size: 0.9em; }80-h1, h2, h3 { line-height: 1.25; }81-/* Section headings: small uppercase monospace on a rule. */82-h2, .label { font: 600 11px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }83-h2 { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin: 28px 0 0; padding-bottom: 6px; border-bottom: 1px solid var(--border); }84-h2 .desc, h2 a { font-weight: 600; }85-h2 .desc { color: var(--faint); }86-h3 { font-size: 1rem; margin: 1.4em 0 0.5em; }163+code, kbd, time, .sha, .mono { font-size: var(--text-inline-mono); }164+.mono .sha, .mono time, .mono code, pre code { font-size: inherit; }165+h1, h2, h3 { line-height: 1.25; font-weight: var(--fw-strong); }166+/* Section headings: sans, strong, on a rule; a count or note after them is quieter. */167+h2 { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2); margin: var(--space-8) 0 0; padding-bottom: var(--space-2); border-bottom: var(--hairline) solid var(--border); font-size: var(--text-md); color: var(--fg); }168+h2 .desc, h2 .n { font-weight: var(--fw-regular); color: var(--faint); }169+h2 .n { font: var(--fw-regular) var(--text-xs) var(--mono); font-variant-numeric: tabular-nums; }170+h2 a { font-weight: var(--fw-medium); }171+h3 { font-size: var(--text-md); margin: var(--space-6) 0 var(--space-2); }172+.label { font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--muted); }87173 .desc, .muted { color: var(--muted); }88174 .i { color: var(--add-fg); }89175 .d { color: var(--del-fg); }90-.wrap { max-width: 1280px; margin: 0 auto; padding: 0 16px; }91-kbd { border: 1px solid var(--border); padding: 0 0.35em; font-size: 0.8em; color: var(--muted); background: var(--subtle); }92-hr { border: 0; border-top: 1px solid var(--border); margin: 1.5em 0; }176+.wrap { max-width: 1280px; margin: 0 auto; padding: 0 var(--space-4); }177+kbd { border: var(--hairline) solid var(--border-strong); padding: 0 var(--space-1); color: var(--muted); background: var(--raised); }178+hr { border: 0; border-top: var(--hairline) solid var(--border); margin: var(--space-6) 0; }179+.icon { display: inline-block; width: 16px; height: 16px; flex: none; vertical-align: -3px; }93180 94181 /* header: on the page, over a rule that the current tab's bar sits on */95-header.site { border-bottom: 1px solid var(--border); }96-.top { display: flex; align-items: center; gap: 10px; padding: 12px 0 8px; min-width: 0; }182+header.site { border-bottom: var(--hairline) solid var(--border); }183+.top { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) 0 var(--space-2); min-width: 0; }97184 .logo { display: block; color: var(--fg); line-height: 0; flex: none; }98185 .mark { display: block; width: 28px; height: 28px; }99-.title { font: 1.05rem var(--mono); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }100-.title a { color: var(--fg); }186+.title { font-size: var(--text-lg); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }187+.title a { color: var(--muted); }101188 .title a:hover { color: var(--deep); }102-.title b { font-weight: 650; }189+.title b, .title b a { font-weight: var(--fw-strong); color: var(--fg); }103190 .top .spacer { flex: 1; }104-.repo-desc { margin: 0 0 10px; color: var(--muted); font-size: 0.93em; }105-nav.tabs { display: flex; gap: 22px; overflow-x: auto; scrollbar-width: none; font: 600 11px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; }191+.repo-desc { margin: 0 0 var(--space-2); color: var(--muted); font-size: var(--text-sm); }192+193+/* tabs: where you are. Words, the current one in ink on the accent bar. */194+nav.tabs { display: flex; gap: var(--space-6); overflow-x: auto; scrollbar-width: none; font-size: var(--text-md); font-weight: var(--fw-medium); }106195 nav.tabs::-webkit-scrollbar { display: none; }107-nav.tabs a { color: var(--muted); padding: 9px 0 10px; white-space: nowrap; }196+nav.tabs a { color: var(--muted); padding: var(--space-2) 0 var(--space-2); white-space: nowrap; }108197 nav.tabs a:hover { color: var(--fg); text-decoration: none; }109-nav.tabs a.cur { color: var(--fg); box-shadow: inset 0 -3px 0 var(--accent); }198+nav.tabs a.cur, nav.tabs a[aria-current="page"], nav.tabs a[aria-current="true"] { color: var(--fg); box-shadow: inset 0 calc(-1 * var(--bar)) 0 var(--accent); }199+nav.tabs .n { margin-left: var(--space-1); font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--faint); font-variant-numeric: tabular-nums; }110200 @media (max-width: 480px) {111- nav.tabs { gap: 16px; }201+ nav.tabs { gap: var(--space-4); }112202 }113203 /* Tabs that don't fit scroll sideways; the edge that has more fades out. */114204 @property --tabs-l { syntax: "<length>"; inherits: false; initial-value: 0px; }115205 @property --tabs-r { syntax: "<length>"; inherits: false; initial-value: 0px; }116206 @supports (animation-timeline: scroll()) {117- nav.tabs { mask-image: linear-gradient(to right, transparent, #000 var(--tabs-l), #000 calc(100% - var(--tabs-r)), transparent); animation: tabs-edge linear both; animation-timeline: scroll(self inline); }207+ nav.tabs { mask-image: linear-gradient(to right, transparent, var(--mask) var(--tabs-l), var(--mask) calc(100% - var(--tabs-r)), transparent); animation: tabs-edge linear both; animation-timeline: scroll(self inline); }118208 @keyframes tabs-edge { from { --tabs-l: 0px; --tabs-r: 32px; } to { --tabs-l: 32px; --tabs-r: 0px; } }119209 }120210 121-/* buttons: square, small uppercase monospace; secondary is the page's122- surface with accent text, primary (.go) is filled with ink */123-button, .btn, details.clone > summary { font: 600 11px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--link); background: var(--subtle); border: 1px solid var(--border); padding: 5px 10px; cursor: pointer; }124-button:hover, .btn:hover { color: var(--deep); background: var(--hover); border-color: var(--faint); text-decoration: none; }125-button[aria-pressed="true"] { color: var(--fg); border-color: var(--fg); }126-button.go, details.clone > summary { color: var(--bg); background: var(--fg); border-color: var(--fg); }127-button.go:hover, details.clone > summary:hover { color: var(--bg); background: var(--muted); border-color: var(--muted); }128-button:disabled { opacity: 0.6; cursor: default; }129-button kbd { font-size: 1em; padding: 0 0.3em; text-transform: none; }211+/* controls (ui.njk). Buttons do things: secondary is a raised surface,212+ primary is filled with the accent. A toggle that is on is filled with213+ ink: an icon toggle, or one segment of a segmented control. A checkbox214+ is on/off with words. Links only navigate. */215+button, .btn, details.clone > summary { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1); min-height: var(--control-h); font: var(--fw-medium) var(--text-sm)/1.2 var(--sans); color: var(--fg); background: var(--raised); border: var(--hairline) solid var(--border-strong); padding: var(--space-1) var(--space-3); cursor: pointer; white-space: nowrap; }216+button:hover, .btn:hover { color: var(--fg); border-color: var(--muted); text-decoration: none; }217+button.primary, details.clone > summary { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }218+button.primary:hover, details.clone > summary:hover, details.clone[open] > summary { color: var(--on-accent); background: var(--accent-hover); border-color: var(--accent-hover); }219+button:disabled { opacity: 0.55; cursor: default; }220+button kbd { padding: 0 var(--space-1); }221+.icon-btn { width: var(--control-h); padding: 0; color: var(--muted); }222+.icon-btn:hover { color: var(--fg); }223+.icon-btn[aria-pressed="true"], .segmented > button[aria-pressed="true"] { color: var(--bg); background: var(--fg); border-color: var(--fg); }224+.segmented { display: inline-flex; }225+.segmented > button + button { margin-left: calc(-1 * var(--hairline)); }226+.segmented > button[aria-pressed="true"] { position: relative; }227+.check { display: inline-flex; align-items: center; gap: var(--space-2); min-height: var(--control-h); font-size: var(--text-sm); color: var(--muted); cursor: pointer; user-select: none; -webkit-user-select: none; white-space: nowrap; }228+.check:has(input:checked) { color: var(--fg); }229+.check input { width: 15px; height: 15px; margin: 0; accent-color: var(--accent); }230+.input, textarea.input { width: 100%; font: var(--fw-regular) var(--text-md)/1.5 var(--sans); color: var(--fg); background: var(--raised); border: var(--hairline) solid var(--border-strong); padding: var(--space-1) var(--space-2); }231+.input.mono { font-family: var(--mono); font-size: var(--text-sm); }232+textarea.input { display: block; min-height: 6em; resize: vertical; padding: var(--space-2) var(--space-3); }233+.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 var(--hairline) var(--accent); }130234 131235 /* clone */132236 details.clone { position: relative; flex: none; }133237 details.clone > summary { list-style: none; }134238 details.clone > summary::-webkit-details-marker { display: none; }135-details.clone .panel { position: absolute; right: 0; top: calc(100% + 6px); z-index: 10; width: min(440px, 92vw); background: var(--raised); border: 1px solid var(--border); padding: 10px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); }136-.copyrow { display: flex; gap: 6px; align-items: center; margin: 4px 0; }137-.clone-note { margin: 6px 0 0; font-size: 0.85em; color: var(--muted); }138-.private-mark { font: 600 10.5px var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--warn); vertical-align: 2px; }239+details.clone .panel { position: absolute; right: 0; top: calc(100% + var(--space-2)); z-index: 10; width: min(440px, 92vw); background: var(--raised); border: var(--hairline) solid var(--border); padding: var(--space-3); box-shadow: var(--shadow); }240+.copyrow { display: flex; gap: var(--space-2); align-items: center; margin: var(--space-1) 0; }241+.copyrow label { width: 44px; font-size: var(--text-sm); color: var(--muted); }242+.copyrow .input { flex: 1; min-width: 0; }243+.clone-note { margin: var(--space-2) 0 0; font-size: var(--text-sm); color: var(--muted); }244+.private-mark { font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--warn); }139245 .private-mark:hover { color: var(--warn); }140-.copyrow label { width: 44px; font: 600 10.5px var(--mono); letter-spacing: 0.08em; color: var(--faint); }141-.copyrow input { flex: 1; min-width: 0; font: 13px var(--mono); padding: 4px 6px; border: 1px solid var(--border); background: var(--bg); color: var(--fg); }142246 143247 /* live status: words, no dot */144-#live { font: 11px var(--mono); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 40vw; }248+#live { font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 40vw; }145249 #live .live-on { color: var(--pass); }146250 #live .live-off { color: var(--faint); }147251 #live .live-busy { color: var(--warn); animation: live-pulse 1s ease-in-out infinite alternate; }148252 @keyframes live-pulse { to { opacity: 0.4; } }149253 150-main.wrap { padding-top: 20px; padding-bottom: 40px; }254+main.wrap { padding-top: var(--space-5); padding-bottom: var(--space-10); }151255 main.wrap > :first-child { margin-top: 0; }152-footer.site { color: var(--muted); padding-bottom: 24px; font-size: 0.85em; }256+footer.site { color: var(--muted); padding-bottom: var(--space-6); font-size: var(--text-sm); }153257 154258 /* toolbars above lists and code */155-.bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 12px; }259+.bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); margin: 0 0 var(--space-3); }156260 .bar .grow { flex: 1; }157-.crumbs { font-family: var(--mono); font-size: 0.95em; overflow-wrap: anywhere; }261+.crumbs { font-family: var(--mono); font-size: var(--text-sm); overflow-wrap: anywhere; }158262 details.picker { position: relative; }159-details.picker > summary { list-style: none; cursor: pointer; border: 1px solid var(--border); padding: 3px 10px; background: var(--subtle); font: 12.5px/1.5 var(--mono); }160-details.picker > summary:hover { background: var(--hover); border-color: var(--faint); }263+details.picker > summary { display: inline-flex; align-items: center; gap: var(--space-2); min-height: var(--control-h); list-style: none; cursor: pointer; border: var(--hairline) solid var(--border-strong); padding: var(--space-1) var(--space-3); background: var(--raised); font: var(--fw-regular) var(--text-sm)/1.2 var(--mono); }264+details.picker > summary b { font-weight: var(--fw-medium); }265+details.picker > summary:hover { border-color: var(--muted); }161266 details.picker > summary::-webkit-details-marker { display: none; }162-details.picker #branch-menu { position: absolute; z-index: 10; top: calc(100% + 4px); left: 0; min-width: 220px; max-height: 60vh; overflow: auto; background: var(--raised); border: 1px solid var(--border); padding: 4px 0; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); display: flex; flex-direction: column; }163-details.picker #branch-menu a { padding: 4px 12px; font-family: var(--mono); font-size: 0.9em; }267+details.picker #branch-menu { position: absolute; z-index: 10; top: calc(100% + var(--space-1)); left: 0; min-width: 220px; max-height: 60vh; overflow: auto; background: var(--raised); border: var(--hairline) solid var(--border); padding: var(--space-1) 0; box-shadow: var(--shadow); display: flex; flex-direction: column; }268+details.picker #branch-menu a { padding: var(--space-1) var(--space-3); font-family: var(--mono); font-size: var(--text-sm); }164269 details.picker #branch-menu a:hover { background: var(--hover); text-decoration: none; }165270 166271 /* lists: tables without outer borders, rows between hairlines, headers in167- small uppercase monospace; they restack on phones */272+ small quiet words; they restack on phones */168273 table.list { width: 100%; border-collapse: collapse; }169-table.list th, table.list td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--border); vertical-align: baseline; }170-table.list thead th { font: 600 10.5px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); padding-top: 10px; padding-bottom: 6px; }274+table.list th, table.list td { text-align: left; padding: var(--space-2); border-bottom: var(--hairline) solid var(--border); vertical-align: baseline; }275+table.list thead th { font-size: var(--text-xs); font-weight: var(--fw-medium); color: var(--muted); padding-top: var(--space-3); padding-bottom: var(--space-1); }171276 table.list .num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }172-table.list td.num, table.list td.date { font: 12.5px/1.6 var(--mono); }277+table.list td.num, table.list td.date { font: var(--fw-regular) var(--text-xs)/1.6 var(--mono); }173278 table.list td.date { color: var(--muted); }174-table.list td.date time { font-size: 1em; }279+table.list td.date time { font-size: inherit; }175280 table.list .nowrap, table.list .date, table.list .sha, table.list td.mono { white-space: nowrap; }281+table.list td.mono { font-size: var(--text-sm); }176282 table.list td.main { width: 100%; overflow-wrap: anywhere; }283+/* A row's own link (a subject, a file, a branch) is in ink; other links stay blue. */284+table.list td.main > a:first-of-type, .files td.main a { color: var(--fg); font-weight: var(--fw-medium); }285+table.list td.main > a:first-of-type:hover, .files td.main a:hover { color: var(--deep); }177286 table.list tbody tr:hover td, table.list tbody tr:focus-within td, table.list tbody tr.sel td { background: var(--hover); }178287 table.list tbody tr.sel td:first-child { box-shadow: inset 2px 0 0 var(--accent); }179288 /* A box holds a list or prose under a heading row; only a file viewer180289 (.viewer) is a bordered region. */181-.box + .box { margin-top: 24px; }182-.box-head { border-bottom: 1px solid var(--border); padding: 8px; display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: baseline; font-size: 0.92em; }290+.box + .box { margin-top: var(--space-6); }291+.box-head { border-bottom: var(--hairline) solid var(--border); padding: var(--space-2); display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); align-items: baseline; font-size: var(--text-sm); }183292 .box-head .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }184-.box-head .label { margin-right: auto; }185-.box.viewer { border: 1px solid var(--border); background: var(--raised); }186-.box.viewer > .box-head { padding: 6px 12px; align-items: center; }293+.box-head .label { margin-right: auto; color: var(--fg); font-weight: var(--fw-strong); }294+.box.viewer { border: var(--hairline) solid var(--border); background: var(--raised); }295+.box.viewer > .box-head { padding: var(--space-1) var(--space-3); align-items: center; }187296 .box.viewer > table.list tr:last-child td { border-bottom: 0; }188-.files td.icon { width: 1.5em; color: var(--muted); padding-right: 0; }297+.files td { padding-top: var(--space-1); padding-bottom: var(--space-1); }298+.files td.icon { width: 1.75em; color: var(--muted); padding-right: 0; }299+.files td.icon .icon { vertical-align: -3px; }189300 .files td.size { color: var(--muted); }190-.empty { color: var(--muted); padding: 24px; text-align: center; }301+.empty { color: var(--muted); padding: var(--space-6); text-align: center; }191302 192303 @media (max-width: 720px) {193304 table.list thead { display: none; }194- table.list.stack tr { display: flex; flex-wrap: wrap; gap: 2px 10px; padding: 9px 8px; border-bottom: 1px solid var(--border); }195- table.list.stack td { border: 0; padding: 0; font-size: 0.85em; color: var(--muted); }196- table.list.stack td.main { flex: 0 0 100%; font-size: 1rem; color: var(--fg); }305+ table.list.stack tr { display: flex; flex-wrap: wrap; gap: var(--space-half) var(--space-3); padding: var(--space-2); border-bottom: var(--hairline) solid var(--border); }306+ table.list.stack td { border: 0; padding: 0; font-size: var(--text-sm); color: var(--muted); }307+ table.list.stack td.main { flex: 0 0 100%; font-size: var(--text-md); color: var(--fg); }197308 /* a stacked row is one block: paint the row, not each cell, so the gaps between cells don't show */198309 table.list.stack tbody tr:hover, table.list.stack tbody tr:focus-within, table.list.stack tbody tr.sel { background: var(--hover); }199310 table.list.stack tbody tr td { background: none; }
@@ -209,16 +320,16 @@ table.list tbody tr.sel td:first-child { box-shadow: inset 2px 0 0 var(--accent)
209320 /* statuses and CI. A review's status is the one filled shape: a small210321 square label, white on the status colour (the same in both modes, so the211322 white stays readable). */212-.status { display: inline-block; font: 600 10px/1.5 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: #fff; background: #5b6169; padding: 0 6px; white-space: nowrap; vertical-align: 1px; }213-.status.open { background: #0e8420; }214-.status.merged { background: #3148f6; }215-.status.closed { background: #c7162b; }216-.ci { white-space: nowrap; font-weight: 600; }323+.status { display: inline-block; font: var(--fw-strong) var(--text-xs)/1.6 var(--sans); color: var(--on-accent); background: var(--status-other); padding: 0 var(--space-2); white-space: nowrap; vertical-align: 1px; }324+.status.open { background: var(--status-open); }325+.status.merged { background: var(--status-merged); }326+.status.closed { background: var(--status-closed); }327+.ci { white-space: nowrap; font-weight: var(--fw-strong); }217328 .ci[title] { cursor: help; }218329 .ci.pass { color: var(--pass); }219330 .ci.fail { color: var(--fail); }220331 .ci.partial, .ci.unverified { color: var(--warn); }221-.st { font-family: var(--mono); font-weight: 600; }332+.st { font-family: var(--mono); font-weight: var(--fw-strong); }222333 .st.A { color: var(--add-fg); }223334 .st.D { color: var(--del-fg); }224335 .st.R, .st.C { color: var(--warn); }
@@ -229,15 +340,15 @@ table.list tbody tr.sel td:first-child { box-shadow: inset 2px 0 0 var(--accent)
229340 scrolls under it: every gutter cell is opaque, its row's tint (translucent230341 in dark mode) layered over the code's background. --w is the widest line231342 number's digit count, set per file, so the columns never overlap. */232-.code { --lnw: max(3.5em, calc(var(--w, 3) * 1ch + 20px)); font: 12.5px/1.5 var(--mono); tab-size: 4; -moz-tab-size: 4; overflow-x: auto; border: 1px solid var(--border); background: var(--raised); }233-.code .rows { display: inline-block; min-width: 100%; padding: 4px 0; }343+.code { --lnw: max(3.5em, calc(var(--w, 3) * 1ch + 20px)); font: var(--fw-regular) var(--text-code)/1.55 var(--mono); tab-size: 4; -moz-tab-size: 4; overflow-x: auto; border: var(--hairline) solid var(--border); background: var(--raised); }344+.code .rows { display: inline-block; min-width: 100%; padding: var(--space-1) 0; }234345 .code .l { --gut: transparent; display: flex; color: var(--fg); text-decoration: none; }235346 .code a.l:hover { text-decoration: none; }236347 .code .ln, .code .mk { position: sticky; background: linear-gradient(var(--gut), var(--gut)) var(--raised); user-select: none; -webkit-user-select: none; }237-.code .ln { flex: none; width: var(--lnw); padding: 0 10px; text-align: right; color: var(--faint); left: 0; }348+.code .ln { flex: none; width: var(--lnw); padding: 0 var(--space-3); text-align: right; color: var(--faint); left: 0; }238349 .code .ln:hover { color: var(--fg); }239350 .code .ln + .ln { left: var(--lnw); }240-.code .lc { white-space: pre; padding: 0 16px 0 6px; flex: 1; }351+.code .lc { white-space: pre; padding: 0 var(--space-4) 0 var(--space-2); flex: 1; }241352 .code .mk { flex: none; width: 1.2em; left: calc(2 * var(--lnw)); text-align: center; color: var(--faint); }242353 .code .l.add { background: var(--add-bg); }243354 .code .l.add .ln { --gut: var(--add-ln); }
@@ -245,11 +356,11 @@ table.list tbody tr.sel td:first-child { box-shadow: inset 2px 0 0 var(--accent)
245356 .code .l.del { background: var(--del-bg); }246357 .code .l.del .ln { --gut: var(--del-ln); }247358 .code .l.del .mk { --gut: var(--del-bg); color: var(--del-fg); }248-.code .l:target { --gut: rgba(255, 213, 79, 0.25); background: var(--gut); }249-.code .l:target .ln, .code .l:target .mk { --gut: rgba(255, 213, 79, 0.35); }359+.code .l:target { --gut: var(--target-bg); background: var(--gut); }360+.code .l:target .ln, .code .l:target .mk { --gut: var(--target-gut); }250361 .code .l.meta { color: var(--muted); font-style: italic; }251-.code .hunk { background: var(--hunk-bg); color: var(--hunk-fg); padding: 2px 12px; white-space: pre; }252-.code .hunk > span { display: inline-block; position: sticky; left: 12px; }362+.code .hunk { background: var(--hunk-bg); color: var(--hunk-fg); padding: var(--space-half) var(--space-3); white-space: pre; }363+.code .hunk > span { display: inline-block; position: sticky; left: var(--space-3); }253364 body.wrap-lines .code .lc { white-space: pre-wrap; overflow-wrap: anywhere; }254365 body.wrap-lines .code .rows { display: block; }255366 body.wrap-lines .code .hunk { white-space: pre-wrap; overflow-wrap: anywhere; }
@@ -258,51 +369,31 @@ body.wrap-lines .code .hunk { white-space: pre-wrap; overflow-wrap: anywhere; }
258369 body:not(.no-wrap) .code .lc, body:not(.no-wrap) .code .hunk { white-space: pre-wrap; overflow-wrap: anywhere; }259370 body:not(.no-wrap) .code .rows { display: block; }260371 }261-section.file { margin: 16px 0; border: 1px solid var(--border); background: var(--raised); }372+section.file { margin: var(--space-4) 0; border: var(--hairline) solid var(--border); background: var(--raised); }262373 .file > .code { border: 0; }263374 .box > .code { border: 0; }264-.fh { position: sticky; top: 0; z-index: 2; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; padding: 8px 12px; background: var(--raised); border-bottom: 1px solid var(--border); font-size: 0.9em; }375+.fh { position: sticky; top: 0; z-index: 2; display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2); padding: var(--space-2) var(--space-3); background: var(--raised); border-bottom: var(--hairline) solid var(--border); font-size: var(--text-sm); }265376 /* A long path shares the line with the status and counts, wrapping at its266377 slashes, rather than wrapping onto a line of its own. */267-.fh .fname { font-family: var(--mono); color: var(--fg); font-weight: 600; overflow-wrap: break-word; flex: 1 1 0; min-width: min(4em, 100%); max-width: max-content; }268-.fh .counts { font-family: var(--mono); font-size: 0.9em; }269-.fh .st { font-size: 0.9em; }270-.fh .view { margin-left: auto; font: 600 10.5px var(--mono); letter-spacing: 0.08em; text-transform: uppercase; }271-.file .note { margin: 0; padding: 10px 12px; color: var(--muted); }272-table.diffstat td { padding: 3px 8px; }273-table.diffstat td.num { font-size: 12px; color: var(--muted); }274-table.diffstat td.graph { font-family: var(--mono); white-space: nowrap; letter-spacing: -1px; }378+.fh .fname { font-family: var(--mono); color: var(--fg); font-weight: var(--fw-medium); overflow-wrap: break-word; flex: 1 1 0; min-width: min(4em, 100%); max-width: max-content; }379+.fh .counts { font: var(--fw-regular) var(--text-xs) var(--mono); }380+.fh .st { font-size: var(--text-xs); }381+.fh .view { margin-left: auto; font-size: var(--text-sm); }382+.file .note { margin: 0; padding: var(--space-3); color: var(--muted); }383+table.diffstat td { padding: var(--space-half) var(--space-2); }384+table.diffstat td.num { font-size: var(--text-xs); color: var(--muted); }385+table.diffstat td.graph { font: var(--fw-regular) var(--text-xs) var(--mono); white-space: nowrap; }275386 table.diffstat td.main { overflow-wrap: break-word; }276-table.diffstat td.main a { font-family: var(--mono); font-size: 0.92em; }387+table.diffstat td.main a { font-family: var(--mono); font-size: var(--text-sm); }277388 /* Phones: the path takes the row and wraps at slashes; the count and graph278389 follow on the same line if they fit, else right-aligned below. */279390 @media (max-width: 720px) {280- table.diffstat tr { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: baseline; gap: 0 8px; padding: 3px 8px; border-bottom: 1px solid var(--border); }391+ table.diffstat tr { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: baseline; gap: 0 var(--space-2); padding: var(--space-1) var(--space-2); border-bottom: var(--hairline) solid var(--border); }281392 table.diffstat td { padding: 0; border: 0; }282393 table.diffstat td.main { flex: 1 1 0; width: auto; min-width: min(10em, 100%); }283394 }284395 285-/* syntax highlighting: highlight.js classes, colors per theme */286-:root {287- --tok-comment: #59636e;288- --tok-keyword: #cf222e;289- --tok-string: #0a3069;290- --tok-const: #0550ae;291- --tok-title: #8250df;292- --tok-builtin: #953800;293- --tok-tag: #116329;294-}295-@media (prefers-color-scheme: dark) {296- :root {297- --tok-comment: #9198a1;298- --tok-keyword: #ff7b72;299- --tok-string: #a5d6ff;300- --tok-const: #79c0ff;301- --tok-title: #d2a8ff;302- --tok-builtin: #ffa657;303- --tok-tag: #7ee787;304- }305-}396+/* syntax highlighting: highlight.js classes, colours from the tokens */306397 .hljs-comment, .hljs-code, .hljs-formula { color: var(--tok-comment); }307398 .hljs-keyword, .hljs-doctag, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable.language_, .hljs-meta .hljs-keyword { color: var(--tok-keyword); }308399 .hljs-string, .hljs-regexp, .hljs-meta .hljs-string { color: var(--tok-string); }
@@ -311,82 +402,88 @@ table.diffstat td.main a { font-family: var(--mono); font-size: 0.92em; }
311402 .hljs-built_in, .hljs-symbol, .hljs-bullet { color: var(--tok-builtin); }312403 .hljs-name, .hljs-quote, .hljs-selector-tag, .hljs-selector-pseudo { color: var(--tok-tag); }313404 .hljs-subst, .hljs-params { color: var(--fg); }314-.hljs-section, .hljs-strong { font-weight: 600; }405+.hljs-section, .hljs-strong { font-weight: var(--fw-strong); }315406 .hljs-emphasis { font-style: italic; }316407 .hljs-addition { color: var(--add-fg); }317408 .hljs-deletion { color: var(--del-fg); }318409 319410 /* commit and review pages */320-.meta-head { margin: 0 0 16px; }321-.meta-head h1 { font-size: 1.3rem; margin: 0 0 6px; overflow-wrap: anywhere; }322-.meta-head .byline { color: var(--muted); font-size: 0.92em; }323-pre.msg { white-space: pre-wrap; overflow-wrap: anywhere; font: 13px/1.5 var(--mono); background: var(--subtle); border: 1px solid var(--border); padding: 12px; margin: 12px 0; }324-details.att > summary, details.howto > summary { cursor: pointer; color: var(--muted); font: 600 11px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; padding: 8px 0; }411+.meta-head { margin: 0 0 var(--space-4); }412+.meta-head h1 { font-size: var(--text-xl); margin: 0 0 var(--space-2); overflow-wrap: anywhere; }413+.meta-head h1 a { color: var(--fg); }414+.meta-head h1 a:hover { color: var(--deep); }415+.meta-head h1 .desc { font: var(--fw-regular) var(--text-lg) var(--mono); color: var(--faint); }416+.meta-head .byline { color: var(--muted); font-size: var(--text-sm); }417+pre.msg { white-space: pre-wrap; overflow-wrap: anywhere; font: var(--fw-regular) var(--text-sm)/1.55 var(--mono); background: var(--raised); border: var(--hairline) solid var(--border); padding: var(--space-3); margin: var(--space-3) 0; }418+details.att > summary, details.howto > summary { cursor: pointer; color: var(--muted); font-size: var(--text-sm); font-weight: var(--fw-medium); padding: var(--space-2) 0; }325419 details.att > summary:hover, details.howto > summary:hover { color: var(--fg); }326-details.howto > summary .jev-sum { text-transform: none; letter-spacing: 0; font: 13px var(--sans); }327-details.att pre, details.howto pre { white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.5 var(--mono); background: var(--subtle); border: 1px solid var(--border); padding: 10px; }420+details.att pre, details.howto pre { white-space: pre-wrap; overflow-wrap: anywhere; font: var(--fw-regular) var(--text-xs)/1.55 var(--mono); background: var(--raised); border: var(--hairline) solid var(--border); padding: var(--space-3); }328421 .depth { color: var(--faint); white-space: pre; }329422 330423 /* Above a diff: the diffstat, with the page's tools on its summary line. */331-.changes { display: grid; grid-template-columns: 1fr auto; align-items: center; column-gap: 12px; }424+.changes { display: grid; grid-template-columns: 1fr auto; align-items: center; column-gap: var(--space-3); }332425 .changes > * { grid-column: 1 / -1; }333-.changes > .tools { grid-column: 2; grid-row: 1; display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 4px 12px; }426+.changes > .tools { grid-column: 2; grid-row: 1; display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: var(--space-1) var(--space-3); }334427 .changes > .tools + * { grid-column: 1; grid-row: 1; }335-.count { color: var(--muted); font-size: 0.9em; white-space: nowrap; }428+.count { color: var(--muted); font-size: var(--text-sm); white-space: nowrap; }336429 @media (max-width: 480px) { .count .wide { display: none; } }337430 .file .code .l { scroll-margin-top: 2.75rem; }338431 339-/* Diff files fold (diffs.ts); review pages add a Viewed box. */432+/* Diff files fold (diffs.ts); review pages add a viewed checkbox. */340433 .fh.foldable { cursor: pointer; }341-.fh .fold { border: 0; background: none; padding: 0; width: 1.1em; color: var(--muted); font: 1.2em/1 var(--sans); letter-spacing: 0; }434+.fh .fold { min-height: 0; border: 0; background: none; padding: 0; width: 1.1em; color: var(--muted); font: var(--fw-regular) var(--text-md)/1 var(--sans); }342435 .fh .fold:hover { background: none; color: var(--fg); }343436 .fh .fold::before { content: "\25BE"; display: inline-block; transition: transform 0.1s; }344437 .file.folded .fold::before { transform: rotate(-90deg); }345438 .file.folded > :not(.fh) { display: none; }346439 .file.folded > .fh { border-bottom: 0; }347-.file.folded:has(.viewed input:checked) .fname { color: var(--muted); font-weight: normal; }348-.fh .viewed { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border: 1px solid var(--border); background: var(--bg); color: var(--muted); font: 600 10.5px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; cursor: pointer; user-select: none; -webkit-user-select: none; }440+.file.folded:has(.viewed input:checked) .fname { color: var(--muted); font-weight: var(--fw-regular); }441+.fh .viewed { margin-left: auto; min-height: 0; }349442 .fh .view ~ .viewed { margin-left: 0; }350-.fh .viewed:has(input:checked) { color: var(--fg); }351-.fh .viewed input { margin: 0; accent-color: var(--accent); }352443 353-/* prose: README, wiki, rendered markdown */354-.markdown { font-size: 1rem; line-height: 1.6; overflow-wrap: break-word; padding: 16px 8px; }444+/* prose: README, wiki, release notes, review descriptions, comments: the445+ serif, for what people wrote */446+.markdown { font: var(--fw-regular) var(--text-prose)/1.65 var(--serif); overflow-wrap: break-word; padding: var(--space-4) var(--space-2); }355447 .markdown > :first-child { margin-top: 0; }356-.markdown h1, .markdown h2 { border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }357-.markdown h1 { font-size: 1.8em; margin: 0.8em 0 0.5em; }358-.markdown h2 { display: block; font: 600 1.4em/1.25 var(--sans); letter-spacing: normal; text-transform: none; color: var(--fg); margin: 1.4em 0 0.6em; }359-.markdown h3 { font-size: 1.15em; }448+.markdown h1, .markdown h2, .markdown h3, .markdown h4 { font-family: var(--sans); font-weight: var(--fw-strong); color: var(--fg); }449+.markdown h1, .markdown h2 { display: block; border-bottom: var(--hairline) solid var(--border); padding-bottom: var(--space-1); }450+.markdown h1 { font-size: var(--text-2xl); margin: var(--space-6) 0 var(--space-3); }451+.markdown h2 { font-size: var(--text-xl); margin: var(--space-6) 0 var(--space-3); }452+.markdown h3 { font-size: var(--text-lg); }453+.markdown h4 { font-size: var(--text-md); }360454 .markdown a:visited { color: var(--deep); }361455 .markdown img { max-width: 100%; height: auto; }362456 .markdown p[align="center"], .markdown div[align="center"] { text-align: center; }363-.markdown code { font-size: 0.875em; background: var(--subtle); border: 1px solid var(--border); padding: 0.05em 0.3em; }364-.markdown pre { font: 13px/1.5 var(--mono); background: var(--subtle); border: 1px solid var(--border); padding: 12px 14px; overflow-x: auto; tab-size: 4; }365-.markdown pre code { background: none; border: 0; padding: 0; font-size: 1em; }457+.markdown code { background: var(--raised); border: var(--hairline) solid var(--border); padding: 0 var(--space-1); }458+.markdown pre { font: var(--fw-regular) var(--text-code)/1.55 var(--mono); background: var(--raised); border: var(--hairline) solid var(--border); padding: var(--space-3) var(--space-4); overflow-x: auto; tab-size: 4; }459+.markdown pre code { background: none; border: 0; padding: 0; }460+/* In the raised file viewer, code sits on the page's colour, not in a second box. */461+.viewer .markdown pre, .viewer .markdown code { background: var(--bg); border-color: var(--bg); }462+.viewer .markdown pre code { background: none; }366463 .markdown pre.mermaid { background: none; border: 0; text-align: center; }367-.markdown blockquote { margin: 1em 0; padding: 0 1em; color: var(--muted); border-left: 3px solid var(--border); }368-.markdown table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: 1em 0; }369-.markdown th, .markdown td { border-bottom: 1px solid var(--border); padding: 6px 12px 6px 0; text-align: left; vertical-align: top; }370-.markdown th { font: 600 10.5px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); padding-top: 8px; }371-.markdown hr { border-top: 1px solid var(--border); }372-.markdown :target { scroll-margin-top: 16px; }464+.markdown blockquote { margin: var(--space-4) 0; padding: 0 var(--space-4); color: var(--muted); border-left: var(--bar) solid var(--border); }465+.markdown table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: var(--space-4) 0; font-size: var(--text-md); }466+.markdown th, .markdown td { border-bottom: var(--hairline) solid var(--border); padding: var(--space-2) var(--space-3) var(--space-2) 0; text-align: left; vertical-align: top; }467+.markdown th { font-family: var(--sans); font-weight: var(--fw-strong); }468+.markdown hr { border-top: var(--hairline) solid var(--border); }469+.markdown :target { scroll-margin-top: var(--space-4); }373470 @media (max-width: 720px) {374- .markdown { padding: 12px 4px; font-size: 0.97rem; }375- .markdown h1 { font-size: 1.5em; }376- .markdown h2 { font-size: 1.25em; }471+ .markdown { padding: var(--space-3) var(--space-1); }472+ .markdown h1 { font-size: var(--text-xl); }473+ .markdown h2 { font-size: var(--text-lg); }377474 }378475 /* jev: commits and repos */379476 /* What Jev says a commit or a repo is: plain words, quiet. */380-.jev-kind { font: 12px var(--mono); color: var(--muted); white-space: nowrap; }477+.jev-kind { font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--muted); white-space: nowrap; }381478 .jev-topic { color: var(--muted); white-space: nowrap; }382-/* Jev's risk score, 0 to 3: a small number, colored once it is moderate. */383-.jev-score { font: 12px var(--mono); font-variant-numeric: tabular-nums; color: var(--faint); white-space: nowrap; }479+/* Jev's risk score, 0 to 3: a small number, coloured once it is moderate. */480+.jev-score { font: var(--fw-regular) var(--text-xs) var(--mono); font-variant-numeric: tabular-nums; color: var(--faint); white-space: nowrap; }384481 .jev-score.moderate { color: var(--warn); }385-.jev-score.high { color: var(--fail); font-weight: 600; }386-.jev-mismatch { font-size: 0.85em; color: var(--faint); cursor: help; }387-.jev-note { font-size: 0.8rem; font-style: italic; color: var(--faint); }482+.jev-score.high { color: var(--fail); font-weight: var(--fw-strong); }483+.jev-mismatch { font-size: var(--text-sm); color: var(--faint); cursor: help; }484+.jev-note { font-size: var(--text-sm); font-style: italic; color: var(--faint); }388485 /* The log: Jev's type in a narrow column on the left. */389-table.log th.kind, table.log td.kind { width: 1%; white-space: nowrap; padding-right: 4px; }486+table.log th.kind, table.log td.kind { width: 1%; white-space: nowrap; padding-right: var(--space-1); }390487 table.log td.risk { width: 1%; }391488 @media (max-width: 720px) {392489 /* Stacked: the type leads the line under the subject. */
@@ -394,18 +491,18 @@ table.log td.risk { width: 1%; }
394491 table.log td.kind { order: 1; }395492 table.log td.kind:empty { display: none; }396493 }397-.releases-link { margin-left: 4px; }398-.notes { padding: 2px 8px 12px; font-size: 0.95em; }399-.notes h3 { margin: 14px 0 4px; font: 600 10.5px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); }400-.notes ul { margin: 0; padding-left: 20px; }401-.notes li { margin: 2px 0; overflow-wrap: anywhere; }402-.notes details { margin-top: 12px; }403-.notes summary { cursor: pointer; font: 600 10.5px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); }494+.releases-link { margin-left: var(--space-1); }495+.notes { padding: var(--space-half) var(--space-2) var(--space-3); font: var(--fw-regular) var(--text-md)/1.6 var(--serif); }496+.notes h3 { margin: var(--space-4) 0 var(--space-1); font: var(--fw-strong) var(--text-sm)/1.4 var(--sans); color: var(--muted); }497+.notes ul { margin: 0; padding-left: var(--space-5); }498+.notes li { margin: var(--space-half) 0; overflow-wrap: anywhere; }499+.notes details { margin-top: var(--space-3); }500+.notes summary { cursor: pointer; font: var(--fw-strong) var(--text-sm)/1.4 var(--sans); color: var(--muted); }404501 .notes summary:hover { color: var(--fg); }405502 406503 /* jev: review pages */407-.jev-mech, .jev-change { font: 12px var(--mono); color: var(--muted); white-space: nowrap; }504+.jev-mech, .jev-change { font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--muted); white-space: nowrap; }408505 .jev-change.behavior, .jev-sum.behavior { color: var(--warn); }409-.jev-sum { font-size: 0.9em; color: var(--muted); }410-summary .jev-sum { margin-left: 6px; font-weight: normal; }506+.jev-sum { font-size: var(--text-sm); font-weight: var(--fw-regular); color: var(--muted); }507+summary .jev-sum { margin-left: var(--space-2); }411508 .comment .tag.jev-addressed { color: var(--pass); }
M site/assets/ui.ts +27 -18
@@ -1,4 +1,4 @@
1-// Small page behaviors: copy buttons, the line-wrap toggle, the find-file1+// Small page behaviors: copy buttons, the wrap-lines checkbox, the find-file22 // button for touch screens, relative times, and diagrams (mermaid, loaded33 // only when a page has one).44 (() => {
@@ -7,33 +7,37 @@
77 const copy = async (text: string, button: HTMLElement): Promise<void> => {88 await navigator.clipboard.writeText(text);99 const was = button.textContent;10- button.textContent = 'Copied';10+ button.textContent = 'copied';1111 setTimeout(() => (button.textContent = was), 1200);1212 };1313 1414 document.addEventListener('click', (e) => {15- const el = (e.target as Element | null)?.closest<HTMLElement>('[data-copy], [data-copy-code], [data-wrap], [data-finder]');15+ const el = (e.target as Element | null)?.closest<HTMLElement>('[data-copy], [data-copy-code], [data-finder]');1616 if (!el) return;1717 if (el.dataset.copy !== undefined) void copy(el.dataset.copy, el);1818 else if (el.hasAttribute('data-copy-code')) {1919 const lines = [...document.querySelectorAll('.code.blob .lc')].map((l) => l.textContent ?? '');2020 void copy(lines.join('\n') + '\n', el);21- } else if (el.hasAttribute('data-wrap')) {22- const on = !document.body.classList.contains('wrap-lines');23- localStorage.setItem('forge.wrap', on ? '1' : '0');24- wrap(on);2521 } else if (el.hasAttribute('data-finder')) {2622 document.dispatchEvent(new KeyboardEvent('keydown', { key: 't' }));2723 }2824 });2925 26+ // The wrap-lines checkbox (ui.checkbox, with data-wrap).27+ document.addEventListener('change', (e) => {28+ const el = e.target as HTMLInputElement;29+ if (!el.matches?.('input[data-wrap]')) return;30+ localStorage.setItem('forge.wrap', el.checked ? '1' : '0');31+ wrap(el.checked);32+ });33+3034 // Long lines wrap on narrow screens unless the reader turned it off, and on3135 // wider ones if they turned it on. The CSS has the same default for pages3236 // without JS; no-wrap marks an explicit off.3337 const wrap = (on: boolean): void => {3438 document.body.classList.toggle('wrap-lines', on);3539 document.body.classList.toggle('no-wrap', !on);36- for (const b of document.querySelectorAll('[data-wrap]')) b.setAttribute('aria-pressed', String(on));40+ for (const b of document.querySelectorAll<HTMLInputElement>('input[data-wrap]')) b.checked = on;3741 };3842 const wrapped = (): boolean => {3943 const v = localStorage.getItem('forge.wrap');
@@ -71,19 +75,24 @@
7175 if (over > 0) tabs.scrollLeft += over + 24;7276 }7377 74- // The index and owner pages' shelves (shelves in macros.njk). Without JS the75- // chips jump to a shelf; here one shows that shelf alone, "has a site"76- // hides repos without one, and the sort switches between newest and a-z.77- // The choices outlive live.ts swapping in a rebuilt page.78+ // The index and owner pages' shelves (shelves in macros.njk). Without JS79+ // the tabs jump to a shelf; here one shows that shelf alone ("all" shows80+ // every one), the globe hides repos without a site, and the sort is81+ // newest first or by name. The choices outlive live.ts swapping in a82+ // rebuilt page.7883 const pick = { shelf: '', site: false, az: false };7984 const shelves = (): void => {8085 const bar = document.querySelector<HTMLElement>('[data-shelves]');8186 if (!bar) return;8287 for (const b of bar.querySelectorAll<HTMLElement>('[hidden]')) b.hidden = false;83- for (const c of bar.querySelectorAll<HTMLElement>('[data-shelf]')) c.setAttribute('aria-pressed', String(c.dataset.shelf === pick.shelf));88+ for (const t of bar.querySelectorAll<HTMLElement>('a[data-shelf]')) {89+ const cur = t.dataset.shelf === pick.shelf;90+ t.classList.toggle('cur', cur);91+ if (cur) t.setAttribute('aria-current', 'page');92+ else t.removeAttribute('aria-current');93+ }8494 bar.querySelector('[data-has-site]')?.setAttribute('aria-pressed', String(pick.site));85- const sort = bar.querySelector<HTMLElement>('[data-sort]');86- if (sort) sort.textContent = pick.az ? 'a-z' : 'newest first';95+ for (const b of bar.querySelectorAll<HTMLElement>('[data-sort]')) b.setAttribute('aria-pressed', String((b.dataset.sort === 'name') === pick.az));8796 let shown = 0;8897 for (const sec of document.querySelectorAll<HTMLElement>('section.shelf')) {8998 const ul = sec.querySelector('ul')!;
@@ -99,12 +108,12 @@
99108 if (empty) empty.hidden = shown > 0;100109 };101110 document.addEventListener('click', (e) => {102- const el = (e.target as Element | null)?.closest<HTMLElement>('[data-shelves] .chip');111+ const el = (e.target as Element | null)?.closest<HTMLElement>('[data-shelves] [data-shelf], [data-shelves] [data-has-site], [data-shelves] [data-sort]');103112 if (!el) return;104113 e.preventDefault();105- if (el.dataset.shelf !== undefined) pick.shelf = pick.shelf === el.dataset.shelf ? '' : el.dataset.shelf;114+ if (el.dataset.shelf !== undefined) pick.shelf = el.dataset.shelf;106115 else if (el.hasAttribute('data-has-site')) pick.site = !pick.site;107- else if (el.hasAttribute('data-sort')) pick.az = !pick.az;116+ else if (el.dataset.sort) pick.az = el.dataset.sort === 'name';108117 shelves();109118 });110119
M site/index.njk +1 -0
M site/insights.njk +1 -0
M site/owner.njk +1 -0
M site/repo/blob.njk +4 -3
M site/repo/commit.njk +11 -10
@@ -4,6 +4,7 @@ pagination:
44 ---55 {% extends "base.njk" %}66 {% import "macros.njk" as m with context %}7+{% import "ui.njk" as ui %}78 {% set c = p.commit %}89 {% set u = p.repo.urls %}910 {% set about = c.author.name ~ ' committed ' ~ c.short ~ ' to ' ~ p.repo.slug ~ ((': ' ~ (c.body | excerpt)) if c.body else (', +' ~ c.stats.add ~ ' -' ~ c.stats.del)) %}
@@ -12,8 +13,8 @@ pagination:
1213 {% endblock %}1314 {% block content %}1415 {% css %}15-dl.facts { display: grid; grid-template-columns: max-content 1fr; align-items: baseline; gap: 4px 16px; margin: 12px 0; font-size: 0.92em; }16-dl.facts dt { font: 600 10.5px/2.1 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); }16+dl.facts { display: grid; grid-template-columns: max-content 1fr; align-items: baseline; gap: var(--space-1) var(--space-4); margin: var(--space-3) 0; font-size: var(--text-sm); }17+dl.facts dt { color: var(--muted); }1718 dl.facts dd { margin: 0; overflow-wrap: anywhere; }1819 {% endcss %}1920 <div class="meta-head">
@@ -22,20 +23,20 @@ dl.facts dd { margin: 0; overflow-wrap: anywhere; }
2223 </div>2324 {% if c.body %}<pre class="msg">{{ c.body }}</pre>{% endif %}2425 <dl class="facts">25-<dt>Commit</dt><dd class="sha">{{ c.sha }}</dd>26-{% for par in c.parents %}<dt>Parent</dt><dd class="sha">{{ par | commitLink(p.repo, par) | safe }}</dd>{% endfor %}27-<dt>Author</dt><dd>{{ c.author.name }} &lt;{{ c.author.email }}&gt;</dd>28-{% if c.ci %}<dt>CI</dt><dd>{{ c.ci | ciHtml(true) | safe }}{% if c.ci.latest.via %} <span class="desc">(same tree as {{ c.ci.latest.via | commitLink(p.repo) | safe }})</span>{% endif %}</dd>{% endif %}29-{% if c.jev %}<dt>Kind</dt><dd>{{ m.jevLabel(c) }}</dd>{% endif %}30-{% for rv in c.reviews %}<dt>Review</dt><dd><a href="{{ u.review(rv.id).href }}">#{{ rv.id }} {{ rv.title }}</a></dd>{% endfor %}26+<dt>commit</dt><dd class="sha">{{ c.sha }}</dd>27+{% for par in c.parents %}<dt>parent</dt><dd class="sha">{{ par | commitLink(p.repo, par) | safe }}</dd>{% endfor %}28+<dt>author</dt><dd>{{ c.author.name }} &lt;{{ c.author.email }}&gt;</dd>29+{% if c.ci %}<dt>ci</dt><dd>{{ c.ci | ciHtml(true) | safe }}{% if c.ci.latest.via %} <span class="desc">(same tree as {{ c.ci.latest.via | commitLink(p.repo) | safe }})</span>{% endif %}</dd>{% endif %}30+{% if c.jev %}<dt>kind</dt><dd>{{ m.jevLabel(c) }}</dd>{% endif %}31+{% for rv in c.reviews %}<dt>review</dt><dd><a href="{{ u.review(rv.id).href }}">#{{ rv.id }} {{ rv.title }}</a></dd>{% endfor %}3132 </dl>3233 {% if c.ci %}33-<details class="att"><summary>CI attestation</summary>34+<details class="att"><summary>ci attestation</summary>3435 <pre>{{ c.ci.latest.payload | statement }}</pre>3536 <p class="desc">Signature: {% if c.ci.latest.verification.ok %}good, {{ c.ci.latest.verification.signer }}{% if c.ci.latest.verification.key %} {{ c.ci.latest.verification.key }}{% endif %}{% else %}{{ c.ci.latest.verification.reason }}{% endif %}</p>3637 </details>3738 {% endif %}38-<div class="changes"><span class="tools"><button type="button" data-wrap>Wrap lines</button></span>39+<div class="changes"><span class="tools">{{ ui.checkbox('wrap lines', false, 'data-wrap') }}</span>3940 {{ c.files | diffstatHtml | safe }}4041 </div>4142 {{ c.files | diffHtml(p.repo) | safe }}
M site/repo/home.njk +2 -1
M site/repo/insights.njk +2 -1
M site/repo/log.njk +1 -0
M site/repo/refs.njk +7 -11
@@ -4,29 +4,25 @@ pagination:
44 ---55 {% extends "base.njk" %}66 {% import "macros.njk" as m with context %}7+{% import "ui.njk" as ui %}78 {% set u = p.repo.urls %}89 {% block content %}9-<h2>Branches</h2>10+{{ ui.heading('branches', p.branches.length) }}1011 <div class="box">11-<table class="list stack nav">12-<thead><tr><th>Branch</th><th>Updated</th><th>Author</th><th class="num">Behind</th><th class="num">Ahead</th><th>Review</th></tr></thead>13-<tbody>12+{% call ui.listTable(['branch', 'updated', 'author', {text: 'behind', cls: 'num'}, {text: 'ahead', cls: 'num'}, 'review']) %}1413 {% for b in p.branches %}1514 <tr><td class="main mono"><a href="{{ u.tree(b.name).href }}">{{ b.name }}</a>{% if b.isDefault %} <span class="desc">default</span>{% endif %}</td><td class="date">{{ b.date | date }}</td><td class="nowrap hide-sm">{{ b.author }}</td><td class="num">{{ ('' ~ b.behind + ' behind') if not b.isDefault else '' }}</td><td class="num">{{ ('' ~ b.ahead + ' ahead') if not b.isDefault else '' }}</td><td>{% if b.review %}<a href="{{ u.review(b.review.id).href }}">#{{ b.review.id }}</a> {{ m.status(b.review.status) }}{% endif %}</td></tr>1615 {% endfor %}17-</tbody>18-</table>16+{% endcall %}1917 </div>2018 {% if p.tags.length %}21-<h2>Tags <a class="releases-link" href="{{ u.releases.href }}">releases</a></h2>19+<h2>tags <span class="n">{{ p.tags.length }}</span> <a class="releases-link" href="{{ u.releases.href }}">releases</a></h2>2220 <div class="box">23-<table class="list stack nav">24-<tbody>21+{% call ui.listTable() %}2522 {% for t in p.tags %}2623 <tr><td class="main mono">{{ t.sha | commitLink(p.repo, t.name) | safe }}</td><td class="date">{{ t.date | date }}</td><td class="hide-sm">{{ t.subject }}</td></tr>2724 {% endfor %}28-</tbody>29-</table>25+{% endcall %}3026 </div>3127 {% endif %}3228 {% endblock %}
M site/repo/releases.njk +8 -7
@@ -4,23 +4,24 @@ pagination:
44 ---55 {% extends "base.njk" %}66 {% import "macros.njk" as m with context %}7+{% import "ui.njk" as ui %}78 {% block content %}89 {% css %}910 /* A release's files: name, size, sha256; then who signed SHA256SUMS. */10-.downloads { padding: 8px 8px 4px; border-bottom: 1px solid var(--border); }11+.downloads { padding: var(--space-2) var(--space-2) var(--space-1); border-bottom: var(--hairline) solid var(--border); }1112 .downloads table { width: 100%; border-collapse: collapse; }12-.downloads td { padding: 3px 12px 3px 0; vertical-align: top; }13-.downloads td.file { white-space: nowrap; font-weight: 600; }14-.downloads td.size { white-space: nowrap; text-align: right; font: 12px var(--mono); }15-.downloads td.sum { font-size: 0.8em; overflow-wrap: anywhere; width: 100%; }16-.downloads p { margin: 6px 0 4px; font-size: 0.9em; }13+.downloads td { padding: var(--space-1) var(--space-3) var(--space-1) 0; vertical-align: top; }14+.downloads td.file { white-space: nowrap; font-weight: var(--fw-medium); }15+.downloads td.size { white-space: nowrap; text-align: right; font: var(--fw-regular) var(--text-xs) var(--mono); }16+.downloads td.sum { font-size: var(--text-xs); overflow-wrap: anywhere; width: 100%; }17+.downloads p { margin: var(--space-2) 0 var(--space-1); font-size: var(--text-sm); }1718 {% endcss %}1819 {# Release notes: each tag's commits since the tag before it, under the1920 section Jev sorted them into (lib/jev-commits.ts). The wording is the2021 commit subjects; commits without an answer are under Other. A tag with2122 a release (forge release publish, lib/releases.ts) lists its files2223 first. #}23-<h2>Releases</h2>24+{{ ui.heading('releases', p.releases.length) }}2425 {% for r in p.releases %}2526 <div class="box" id="{{ r.tag.name }}">2627 <div class="box-head"><b class="mono">{{ r.tag.sha | commitLink(p.repo, r.tag.name) | safe }}</b><span class="grow desc">{{ ('since ' ~ r.prev) if r.prev else 'first release' }}, {{ r.count | plural('commit') }}{{ ' or more' if r.more }}</span><span class="date">{{ r.tag.date | date }}</span></div>
M site/repo/review-diff.njk +17 -17
@@ -4,6 +4,7 @@ pagination:
44 ---55 {% extends "base.njk" %}66 {% import "macros.njk" as m with context %}7+{% import "ui.njk" as ui %}78 {% set rv = p.review %}89 {% block head %}{{ m.reviewMeta(rv, p.repo, p.title, p.repo.urls.reviewDiff(rv.id).href) }}{% endblock %}910 {% block scripts %}<script src="{{ assets.diffs }}" defer></script>
@@ -12,40 +13,39 @@ pagination:
1213 {% block content %}1314 {% css %}1415 /* Line comments under their lines; they look like the discussion's (discussion.njk). */15-.cmt { border-top: 1px solid var(--border); }16-.cmt:target { box-shadow: inset 3px 0 0 var(--accent); }17-.cmt-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; padding: 8px 12px 0; font-size: 0.9em; }18-.cmt-head a, .cmt-head .date { font: 12px var(--mono); }19-.cmt-head time { font-size: 1em; }20-.cmt-head .tag { font: 600 10.5px var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--warn); }16+.cmt { border-top: var(--hairline) solid var(--border); }17+.cmt:target { box-shadow: inset var(--bar) 0 0 var(--accent); }18+.cmt-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-half) var(--space-3); padding: var(--space-2) var(--space-3) 0; font-size: var(--text-sm); }19+.cmt-head a, .cmt-head .date { font: var(--fw-regular) var(--text-xs) var(--mono); }20+.cmt-head time { font-size: inherit; }21+.cmt-head .tag { font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--warn); }2122 .cmt-head a { color: var(--muted); }22-.cmt .markdown { padding: 4px 12px 8px; font-size: 0.95rem; line-height: 1.55; }23+.cmt .markdown { padding: var(--space-1) var(--space-3) var(--space-2); font-size: var(--text-md); line-height: 1.6; }2324 .cmt .markdown > :last-child { margin-bottom: 0; }2425 /* In a diff, a comment is as wide as the code box, not its longest line. */2526 .code:has(.cmts) { container-type: inline-size; }26-.code .cmts { position: sticky; left: 0; width: 100cqw; padding: 0 10px 8px; background: var(--bg); border-block: 1px solid var(--border); font: 15px/1.5 var(--sans); white-space: normal; }27+.code .cmts { position: sticky; left: 0; width: 100cqw; padding: 0 var(--space-3) var(--space-2); background: var(--bg); border-block: var(--hairline) solid var(--border); font: var(--fw-regular) var(--text-md)/1.5 var(--sans); white-space: normal; }2728 .code .cmts > .cmt:first-child { border-top: 0; }28-.code .cmts > form { padding-top: 8px; }29+.code .cmts > form { padding-top: var(--space-2); }2930 /* A new line comment, signed in (discuss.ts): the + on the line under the3031 pointer (or the line last clicked) opens a form under it. */3132 .commenting .code.diff .rows { position: relative; }32-.addline { position: absolute; z-index: 2; width: 20px; height: 17px; padding: 0; font: 600 13px/15px var(--sans); letter-spacing: 0; text-align: center; color: #fff; background: var(--accent); border-color: var(--accent); }33-.addline:hover { color: #fff; background: var(--deep); border-color: var(--deep); }34-.cmts .where { font: 12px/1.6 var(--mono); color: var(--muted); margin-bottom: 4px; overflow-wrap: anywhere; }35-.cmts textarea { display: block; width: 100%; min-height: 5em; resize: vertical; font: 14px/1.5 var(--sans); color: var(--fg); background: var(--raised); border: 1px solid var(--border); padding: 8px 10px; }36-.cmts textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }37-.cmts .reply-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; margin-top: 8px; font-size: 0.92em; }33+.addline { position: absolute; z-index: 2; width: 20px; height: 17px; min-height: 0; padding: 0; font: var(--fw-strong) var(--text-sm)/15px var(--sans); text-align: center; color: var(--on-accent); background: var(--accent); border-color: var(--accent); }34+.addline:hover { color: var(--on-accent); background: var(--accent-hover); border-color: var(--accent-hover); }35+.cmts .where { font: var(--fw-regular) var(--text-xs)/1.6 var(--mono); color: var(--muted); margin-bottom: var(--space-1); overflow-wrap: anywhere; }36+.cmts textarea { min-height: 5em; }37+.cmts .reply-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-2); font-size: var(--text-sm); }3838 .cmts .reply-bar .grow { flex: 1; }3939 .cmts .reply-status.bad { color: var(--fail); white-space: pre-line; }4040 .cmt.pending { border-top-style: dashed; }41-.cmt.pending .text { padding: 8px 12px; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--muted); }41+.cmt.pending .text { padding: var(--space-2) var(--space-3); white-space: pre-wrap; overflow-wrap: anywhere; color: var(--muted); }4242 {% endcss %}4343 <div class="meta-head">4444 <h1><a href="{{ p.repo.urls.review(rv.id).href }}" data-key="u">{{ rv.title }}</a> <span class="desc">#{{ rv.id }}</span></h1>4545 <div class="byline">Combined diff of {{ rv.layer.length | plural('commit') }} on <span class="mono">{{ rv.branch }}</span></div>4646 </div>4747 <p class="desc" id="line-comments" data-post="{{ p.repo.urls.review(rv.id).href }}" data-at="{{ rv.tip }}" hidden></p>48-<div class="changes"><span class="tools"><span class="count" data-viewed="{{ p.repo.slug }}" hidden></span><button type="button" data-wrap>Wrap lines</button></span>48+<div class="changes"><span class="tools"><span class="count" data-viewed="{{ p.repo.slug }}" hidden></span>{{ ui.checkbox('wrap lines', false, 'data-wrap') }}</span>4949 {{ rv.combined | diffstatHtml | safe }}5050 </div>5151 {{ rv.combined | diffHtml(p.repo, '', rv.branch if rv.live else null, rv, true) | safe }}
M site/repo/review.njk +20 -18
@@ -4,6 +4,7 @@ pagination:
44 ---55 {% extends "base.njk" %}66 {% import "macros.njk" as m with context %}7+{% import "ui.njk" as ui %}78 {% set rv = p.review %}89 {% set u = p.repo.urls %}910 {% block head %}{{ m.reviewMeta(rv, p.repo, p.title, u.review(rv.id).href) }}{% endblock %}
@@ -12,24 +13,25 @@ pagination:
1213 {% endblock %}1314 {% block content %}1415 {% css %}15-.review-desc { margin: 0 0 16px; border-block: 1px solid var(--border); }16+.review-desc { margin: 0 0 var(--space-4); border-block: var(--hairline) solid var(--border); }1617 .review-desc .markdown > :last-child { margin-bottom: 0; }17-.stacked { margin-top: 28px; }18-.stacked > h3 { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; margin: 0 0 4px; padding-top: 12px; border-top: 1px solid var(--border); font-size: 1rem; }19-.stacked > h3 .step { color: var(--faint); font: 600 11px var(--mono); letter-spacing: 0.08em; }20-#stack tr.this td { font-weight: 600; }18+.stacked { margin-top: var(--space-8); }19+.stacked > h3 { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3); align-items: baseline; margin: 0 0 var(--space-1); padding-top: var(--space-3); border-top: var(--hairline) solid var(--border); font-size: var(--text-lg); }20+.stacked > h3 .step { color: var(--faint); font: var(--fw-regular) var(--text-xs) var(--mono); }21+#stack tr.this td { font-weight: var(--fw-strong); }22+.review-tools { margin-top: var(--space-3); }2123 /* The sticky bar: layers of the stack, commits of the layer. It has a fixed2224 height so file headers can stick right below it. */23-.revnav { position: sticky; top: 0; z-index: 3; height: 2.75rem; display: flex; align-items: center; gap: 12px; margin: 28px 0 0; background: var(--bg); border-bottom: 1px solid var(--border); white-space: nowrap; overflow-x: auto; scrollbar-width: none; font: 12px var(--mono); }25+.revnav { position: sticky; top: 0; z-index: 3; height: 2.75rem; display: flex; align-items: center; gap: var(--space-3); margin: var(--space-8) 0 0; background: var(--bg); border-bottom: var(--hairline) solid var(--border); white-space: nowrap; overflow-x: auto; scrollbar-width: none; font: var(--fw-regular) var(--text-xs) var(--mono); }2426 .revnav::-webkit-scrollbar { display: none; }2527 @media (max-width: 480px) {26- .revnav { gap: 8px; }27- .revnav .seg > * { padding: 3px 8px; }28+ .revnav { gap: var(--space-2); }29+ .revnav .seg > * { padding: var(--space-1) var(--space-2); }2830 .revnav .n { display: none; }2931 }30-.revnav .seg { display: inline-flex; align-items: stretch; border: 1px solid var(--border); background: var(--subtle); overflow: hidden; }31-.revnav .seg > * { padding: 3px 10px; }32-.revnav .seg > .at { color: var(--fg); border-inline: 1px solid var(--border); font-variant-numeric: tabular-nums; }32+.revnav .seg { display: inline-flex; align-items: stretch; border: var(--hairline) solid var(--border-strong); background: var(--raised); overflow: hidden; }33+.revnav .seg > * { padding: var(--space-1) var(--space-3); }34+.revnav .seg > .at { color: var(--fg); border-inline: var(--hairline) solid var(--border); font-variant-numeric: tabular-nums; }3335 .revnav .seg > a:hover { background: var(--hover); text-decoration: none; }3436 .revnav .seg > a:not([href]) { color: var(--faint); }3537 .revnav .grow { flex: 1; }
@@ -45,7 +47,7 @@ pagination:
4547 {% if rv.opts.description %}<div class="box review-desc"><div class="markdown">{{ rv.opts.description | reviewMarkdown(p.repo, rv, true) | safe }}</div></div>{% endif %}4648 4749 {% if p.stack.length > 1 %}48-<h2>Stack</h2>50+{{ ui.heading('stack', p.stack.length) }}4951 <div class="box">5052 <table class="list stack" id="stack">5153 <tbody>
@@ -58,7 +60,7 @@ pagination:
5860 </div>5961 {% endif %}6062 61-<h2>Commits <span class="desc">oldest first</span></h2>63+{{ ui.heading('commits', rv.layer.length, 'oldest first') }}6264 <div class="box">6365 <table class="list stack nav" id="layer">6466 <tbody>
@@ -69,10 +71,10 @@ pagination:
6971 </table>7072 </div>7173 72-<div class="bar" style="margin-top:12px">73-<a href="{{ u.reviewDiff(rv.id).href }}">Combined diff</a> <span class="desc">{{ rv.combinedStats.files | plural('file') }}, <span class="i">+{{ rv.combinedStats.add }}</span> <span class="d">-{{ rv.combinedStats.del }}</span></span>74-{% for s in rv.previews %}<span>Preview: <a href="{{ s.url }}">{{ s.name }}</a>{% if s.history[0].source != rv.tip %} <span class="ci partial" title="the branch has moved since this preview was built">stale</span>{% endif %}</span>{% endfor %}75-<span class="grow"></span>{% if p.stack.length < 2 and rv.layer.length < 2 %}<span class="count" data-viewed="{{ p.repo.slug }}" hidden></span>{% endif %}<button type="button" data-wrap>Wrap lines</button>74+<div class="bar review-tools">75+<a href="{{ u.reviewDiff(rv.id).href }}">combined diff</a> <span class="desc">{{ rv.combinedStats.files | plural('file') }}, <span class="i">+{{ rv.combinedStats.add }}</span> <span class="d">-{{ rv.combinedStats.del }}</span></span>76+{% for s in rv.previews %}<span>preview <a href="{{ s.url }}">{{ s.name }}</a>{% if s.history[0].source != rv.tip %} <span class="ci partial" title="the branch has moved since this preview was built">stale</span>{% endif %}</span>{% endfor %}77+<span class="grow"></span>{% if p.stack.length < 2 and rv.layer.length < 2 %}<span class="count" data-viewed="{{ p.repo.slug }}" hidden></span>{% endif %}{{ ui.checkbox('wrap lines', false, 'data-wrap') }}7678 </div>7779 7880 {% if rv.revisions.length > 1 %}
@@ -88,7 +90,7 @@ pagination:
8890 {% endif %}8991 9092 {% if rv.status == 'open' or rv.status == 'draft' %}91-<details class="howto"><summary>Check out or land</summary>93+<details class="howto"><summary>check out or land</summary>9294 <pre>git fetch {{ u.cloneSsh }} {{ rv.branch }} &amp;&amp; git switch --detach FETCH_HEAD9395 forge checkout {{ rv.id }} # the same, on a local tracking branch9496 forge attest --stack # CI for each commit, on your machine
M site/repo/reviews.njk +6 -4
M site/repo/revision.njk +11 -10
@@ -4,6 +4,7 @@ pagination:
44 ---55 {% extends "base.njk" %}66 {% import "macros.njk" as m with context %}7+{% import "ui.njk" as ui %}78 {% set rv = p.review %}89 {% block head %}{{ m.reviewMeta(rv, p.repo, p.title, p.repo.urls.revision(rv.id, p.rev.n).href) }}{% endblock %}910 {% block scripts %}<script src="{{ assets.diffs }}" defer></script>
@@ -11,23 +12,23 @@ pagination:
1112 {% block content %}1213 {% css %}1314 /* range-diff: a block per pair of commits */14-.pk { font: 600 10.5px var(--mono); text-transform: uppercase; letter-spacing: 0.08em; }15+.pk { font-size: var(--text-sm); font-weight: var(--fw-strong); }1516 .pk.same { color: var(--muted); }1617 .pk.changed { color: var(--warn); }1718 .pk.add { color: var(--add-fg); }1819 .pk.del { color: var(--del-fg); }19-.subj { font-weight: 600; overflow-wrap: anywhere; }20-.pairrow { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin: 0; padding: 8px; border-bottom: 1px solid var(--border); font-size: 0.9em; }21-details.pairs { margin: 0; padding: 8px; border-bottom: 1px solid var(--border); font-size: 0.9em; }22-details.pairs > summary { cursor: pointer; color: var(--muted); font: 600 11px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; }23-details.pairs .pairrow { margin: 0; padding: 6px 0 0; border: 0; }20+.subj { font-weight: var(--fw-strong); overflow-wrap: anywhere; }21+.pairrow { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-3); margin: 0; padding: var(--space-2); border-bottom: var(--hairline) solid var(--border); font-size: var(--text-sm); }22+details.pairs { margin: 0; padding: var(--space-2); border-bottom: var(--hairline) solid var(--border); font-size: var(--text-sm); }23+details.pairs > summary { cursor: pointer; color: var(--muted); font-weight: var(--fw-medium); }24+details.pairs .pairrow { margin: 0; padding: var(--space-2) 0 0; border: 0; }2425 .pair .fh .subj { color: var(--fg); }2526 2627 /* No line numbers here: the marker sticks at the left edge, not after them. */2728 .code.range .mk { left: 0; }28-.code.range .l.sect .lc { font-weight: 600; }29-.code.range .lc .i { color: var(--add-fg); font-weight: 600; }30-.code.range .lc .d { color: var(--del-fg); font-weight: 600; }29+.code.range .l.sect .lc { font-weight: var(--fw-strong); }30+.code.range .lc .i { color: var(--add-fg); font-weight: var(--fw-strong); }31+.code.range .lc .d { color: var(--del-fg); font-weight: var(--fw-strong); }3132 {% endcss %}3233 <div class="meta-head">3334 <h1><a href="{{ p.repo.urls.review(rv.id).href }}" data-key="u">{{ rv.title }}</a> <span class="desc">#{{ rv.id }}</span></h1>
@@ -35,7 +36,7 @@ details.pairs .pairrow { margin: 0; padding: 6px 0 0; border: 0; }
3536 {% if p.rev.view == 'diff' %}3637 <div class="byline">What changed from r{{ p.rev.prev }} ({{ p.from.sha | commitLink(p.repo) | safe }}) to r{{ p.rev.n }} ({{ p.rev.sha | commitLink(p.repo) | safe }}), pushed {{ p.rev.ts | ago }}. Both are on the same base, so this is the diff between their tips{% if rv.parent %}, including any change to the layers below{% endif %}.</div>3738 </div>38-<div class="changes"><span class="tools"><button type="button" data-wrap>Wrap lines</button></span>39+<div class="changes"><span class="tools">{{ ui.checkbox('wrap lines', false, 'data-wrap') }}</span>3940 {{ p.files | diffstatHtml | safe }}4041 </div>4142 {% if not p.files.length %}<p class="empty">The trees are identical: only commit messages or history changed.</p>{% endif %}
M site/repo/sites.njk +2 -1
M site/repo/tree.njk +2 -1
M site/repo/wiki.njk +9 -8
@@ -4,20 +4,21 @@ pagination:
44 ---55 {% extends "base.njk" %}66 {% import "macros.njk" as m with context %}7+{% import "ui.njk" as ui %}78 {% block head %}{{ m.meta(p.title, p.wiki.html | excerpt(true), p.repo.urls.wiki(p.wiki.slug).href) }}{% endblock %}89 {% block content %}910 {% css %}1011 /* wiki */11-.wiki-layout { display: grid; grid-template-columns: 220px 1fr; gap: 24px; }12-.wiki-nav { border-top: 1px solid var(--border); }13-.wiki-nav a { display: block; padding: 6px 8px; border-bottom: 1px solid var(--border); color: var(--fg); }12+.wiki-layout { display: grid; grid-template-columns: 220px 1fr; gap: var(--space-6); }13+.wiki-nav { border-top: var(--hairline) solid var(--border); }14+.wiki-nav a { display: block; padding: var(--space-2); border-bottom: var(--hairline) solid var(--border); color: var(--fg); }1415 .wiki-nav a:hover { background: var(--hover); text-decoration: none; }15-.wiki-nav a.cur { font-weight: 600; box-shadow: inset 2px 0 0 var(--accent); }16+.wiki-nav a.cur { font-weight: var(--fw-strong); box-shadow: inset 2px 0 0 var(--accent); }1617 @media (max-width: 720px) {17- .wiki-layout { grid-template-columns: 1fr; gap: 8px; }18- .wiki-nav { display: flex; overflow-x: auto; gap: 16px; border-top: 0; border-bottom: 1px solid var(--border); }19- .wiki-nav a { white-space: nowrap; padding: 6px 0; border: 0; }20- .wiki-nav a.cur { box-shadow: inset 0 -2px 0 var(--accent); }18+ .wiki-layout { grid-template-columns: 1fr; gap: var(--space-2); }19+ .wiki-nav { display: flex; overflow-x: auto; gap: var(--space-4); border-top: 0; border-bottom: var(--hairline) solid var(--border); }20+ .wiki-nav a { white-space: nowrap; padding: var(--space-2) 0; border: 0; }21+ .wiki-nav a.cur { box-shadow: inset 0 calc(-1 * var(--bar)) 0 var(--accent); }2122 }2223 {% endcss %}2324 <div class="wiki-layout">
M test/scenarios/insights-pages.sh +1 -1
M test/scenarios/log-columns.sh +1 -1
M test/scenarios/release-page.sh +2 -2
M test/scenarios/release-tab.sh +1 -1
M test/scenarios/web-comment-page.sh +1 -1