detached 8e01339

fix(web): rail nits: no idle scroll bar on files, merge rule as a note, latest tag in About, wider commit subjects (#55)

commit
8e01339264a048783f64b93f1dd183137258853b
author
brice · · 2026-10-18 06:07 UTC
committer
bnema · · 2026-10-18 06:07 UTC
parent
032b9ef
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
9 files · +473 -423
M internal/adapters/web/actions.templ +1 -1
@@ -462,7 +462,7 @@ templ MRChecks(c ChecksSection) {
462462 </ul>
463463 }
464464 if c.Note != "" {
465- <p class="mute">{ c.Note }</p>
465+ <p class="rule-note"><span class="sr">note: </span>{ c.Note }</p>
466466 }
467467 </section>
468468 }
@@ -462,7 +462,7 @@ templ MRChecks(c ChecksSection) {
462 </ul>462 </ul>
463 }463 }
464 if c.Note != "" {464 if c.Note != "" {
465- <p class="mute">{ c.Note }</p>465+ <p class="rule-note"><span class="sr">note: </span>{ c.Note }</p>
466 }466 }
467 </section>467 </section>
468 }468 }
M internal/adapters/web/actions_templ.go +2 -2 generated file: collapsed
M internal/adapters/web/pages.templ +27 -18
@@ -54,28 +54,37 @@ templ Summary(l Layout, p SummaryPage) {
5454 }
5555 }
5656
57+// latestTagFact is the "latest tag" row of About: the newest tag (its
58+// commit subject on hover), how old it is, and the count of tags, a link to
59+// all of them. With too many tags to date them, the count only.
60+templ latestTagFact(t LatestTag) {
61+ <dt>latest tag</dt>
62+ <dd class="latest-tag">
63+ if t.Name != "" {
64+ <a class="tag-name" href={ templ.SafeURL(t.Href) } title={ t.Subject }><span class="ico i-tag" aria-hidden="true"></span>{ t.Name }</a>
65+ if t.WhenAt != "" {
66+ <time class="dim" datetime={ t.WhenAt } title={ t.WhenAt }>{ t.When }</time>
67+ }
68+ }
69+ <a class="all-tags" href={ templ.SafeURL(t.RefsHref) }>{ plural(t.Count, "tag", "tags") }</a>
70+ </dd>
71+}
72+
5773 templ summaryRail(p SummaryPage) {
5874 <section aria-labelledby="r-clone"><h2 id="r-clone">Clone</h2>@CloneBlock(p.Clone)
5975 </section>
60- <section aria-labelledby="r-facts"><h2 id="r-facts">About</h2>@Facts(p.Facts)
61-</section>
62- if p.Latest != nil {
63- <section aria-labelledby="r-tag">
64- <h2 id="r-tag">Tags <span class="count">{ itoa(p.Latest.Count) }</span> <a href={ templ.SafeURL(p.Latest.RefsHref) }>View all</a></h2>
65- if p.Latest.Name != "" {
66- <div class="latest-tag">
67- <a class="tag-name" href={ templ.SafeURL(p.Latest.Href) }><span class="ico i-tag" aria-hidden="true"></span>{ p.Latest.Name }</a>
68- <span class="pill">latest</span>
69- <time class="dim" datetime={ p.Latest.WhenAt } title={ p.Latest.WhenAt }>{ p.Latest.When }</time>
70- if p.Latest.Subject != "" {
71- <p class="tag-sub dim">{ p.Latest.Subject }</p>
72- }
73- </div>
74- } else {
75- <p class="dim">{ plural(p.Latest.Count, "tag", "tags") }</p>
76+ <section aria-labelledby="r-facts">
77+ <h2 id="r-facts">About</h2>
78+ <dl class="kv">
79+ for _, f := range p.Facts {
80+ <dt>{ f.Key }</dt>
81+ @FactValue(f)
7682 }
77- </section>
78- }
83+ if p.Latest != nil {
84+ @latestTagFact(*p.Latest)
85+ }
86+ </dl>
87+ </section>
7988 if len(p.Recent) > 0 {
8089 <section aria-labelledby="r-recent"><h2 id="r-recent">Recent commits <a href={ templ.SafeURL(p.AllHref) }>View all</a></h2>@CommitRows(p.Recent)
8190 </section>
@@ -54,28 +54,37 @@ templ Summary(l Layout, p SummaryPage) {
54 }54 }
55 }55 }
56 56
57+// latestTagFact is the "latest tag" row of About: the newest tag (its
58+// commit subject on hover), how old it is, and the count of tags, a link to
59+// all of them. With too many tags to date them, the count only.
60+templ latestTagFact(t LatestTag) {
61+ <dt>latest tag</dt>
62+ <dd class="latest-tag">
63+ if t.Name != "" {
64+ <a class="tag-name" href={ templ.SafeURL(t.Href) } title={ t.Subject }><span class="ico i-tag" aria-hidden="true"></span>{ t.Name }</a>
65+ if t.WhenAt != "" {
66+ <time class="dim" datetime={ t.WhenAt } title={ t.WhenAt }>{ t.When }</time>
67+ }
68+ }
69+ <a class="all-tags" href={ templ.SafeURL(t.RefsHref) }>{ plural(t.Count, "tag", "tags") }</a>
70+ </dd>
71+}
72+
57 templ summaryRail(p SummaryPage) {73 templ summaryRail(p SummaryPage) {
58 <section aria-labelledby="r-clone"><h2 id="r-clone">Clone</h2>@CloneBlock(p.Clone)74 <section aria-labelledby="r-clone"><h2 id="r-clone">Clone</h2>@CloneBlock(p.Clone)
59 </section>75 </section>
60- <section aria-labelledby="r-facts"><h2 id="r-facts">About</h2>@Facts(p.Facts)76+ <section aria-labelledby="r-facts">
61-</section>77+ <h2 id="r-facts">About</h2>
62- if p.Latest != nil {78+ <dl class="kv">
63- <section aria-labelledby="r-tag">79+ for _, f := range p.Facts {
64- <h2 id="r-tag">Tags <span class="count">{ itoa(p.Latest.Count) }</span> <a href={ templ.SafeURL(p.Latest.RefsHref) }>View all</a></h2>80+ <dt>{ f.Key }</dt>
65- if p.Latest.Name != "" {81+ @FactValue(f)
66- <div class="latest-tag">
67- <a class="tag-name" href={ templ.SafeURL(p.Latest.Href) }><span class="ico i-tag" aria-hidden="true"></span>{ p.Latest.Name }</a>
68- <span class="pill">latest</span>
69- <time class="dim" datetime={ p.Latest.WhenAt } title={ p.Latest.WhenAt }>{ p.Latest.When }</time>
70- if p.Latest.Subject != "" {
71- <p class="tag-sub dim">{ p.Latest.Subject }</p>
72- }
73- </div>
74- } else {
75- <p class="dim">{ plural(p.Latest.Count, "tag", "tags") }</p>
76 }82 }
77- </section>83+ if p.Latest != nil {
78- }84+ @latestTagFact(*p.Latest)
85+ }
86+ </dl>
87+ </section>
79 if len(p.Recent) > 0 {88 if len(p.Recent) > 0 {
80 <section aria-labelledby="r-recent"><h2 id="r-recent">Recent commits <a href={ templ.SafeURL(p.AllHref) }>View all</a></h2>@CommitRows(p.Recent)89 <section aria-labelledby="r-recent"><h2 id="r-recent">Recent commits <a href={ templ.SafeURL(p.AllHref) }>View all</a></h2>@CommitRows(p.Recent)
81 </section>90 </section>
M internal/adapters/web/pages_templ.go +398 -369 generated file: collapsed
M internal/adapters/web/static/css/20-shell.css +8 -6
@@ -134,13 +134,15 @@
134134 .rail section > .list { margin: var(--s2) 0; }
135135 .rail section > p + p { margin-top: calc(-1 * var(--s2)); }
136136
137- /* newest tag in the summary rail */
138- .latest-tag {
139- display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1) var(--s2); font-size: var(--fs-s);
140- & .tag-name { display: inline-flex; align-items: center; gap: 6px; font: 700 var(--fs) / 20px var(--mono); color: var(--ink-hi); text-decoration: none; }
137+ /* newest tag, a row of About in the summary rail */
138+ .kv dd.latest-tag {
139+ display: flex; align-items: baseline; gap: var(--s2);
140+ & .tag-name { display: inline-flex; align-items: center; gap: 4px; min-width: 0; overflow: hidden; font-weight: 700; font-family: var(--mono); color: var(--ink-hi); text-decoration: none; }
141+ & .tag-name .ico { flex: none; }
141142 & .tag-name:hover { text-decoration: underline; }
142- & time { margin-left: auto; }
143- & .tag-sub { flex-basis: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
143+ & time { white-space: nowrap; }
144+ & .all-tags { margin-left: auto; white-space: nowrap; color: var(--ink-dim); text-decoration: none; }
145+ & .all-tags:hover { color: var(--ink-hi); text-decoration: underline; }
144146 }
145147
146148 .kv {
@@ -134,13 +134,15 @@
134 .rail section > .list { margin: var(--s2) 0; }134 .rail section > .list { margin: var(--s2) 0; }
135 .rail section > p + p { margin-top: calc(-1 * var(--s2)); }135 .rail section > p + p { margin-top: calc(-1 * var(--s2)); }
136 136
137- /* newest tag in the summary rail */137+ /* newest tag, a row of About in the summary rail */
138- .latest-tag {138+ .kv dd.latest-tag {
139- display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1) var(--s2); font-size: var(--fs-s);139+ display: flex; align-items: baseline; gap: var(--s2);
140- & .tag-name { display: inline-flex; align-items: center; gap: 6px; font: 700 var(--fs) / 20px var(--mono); color: var(--ink-hi); text-decoration: none; }140+ & .tag-name { display: inline-flex; align-items: center; gap: 4px; min-width: 0; overflow: hidden; font-weight: 700; font-family: var(--mono); color: var(--ink-hi); text-decoration: none; }
141+ & .tag-name .ico { flex: none; }
141 & .tag-name:hover { text-decoration: underline; }142 & .tag-name:hover { text-decoration: underline; }
142- & time { margin-left: auto; }143+ & time { white-space: nowrap; }
143- & .tag-sub { flex-basis: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }144+ & .all-tags { margin-left: auto; white-space: nowrap; color: var(--ink-dim); text-decoration: none; }
145+ & .all-tags:hover { color: var(--ink-hi); text-decoration: underline; }
144 }146 }
145 147
146 .kv {148 .kv {
M internal/adapters/web/static/css/50-code.css +15 -9
@@ -115,7 +115,9 @@
115115 font-size: var(--fs-s); position: relative; /* the offsets of its links count from it (ermite.js) */
116116 & li[hidden] { display: none; }
117117 & a {
118- display: grid; grid-template-columns: 1.5ch minmax(0, 1fr) auto auto; column-gap: var(--s2); align-items: baseline;
118+ /* centred, not on the baseline: the smaller counts on a baseline sit
119+ a pixel below the row, enough to give the list a scroll bar */
120+ display: grid; grid-template-columns: 1.5ch minmax(0, 1fr) auto auto; column-gap: var(--s2); align-items: center;
119121 height: 26px; line-height: 26px; padding: 0 var(--s2); margin: 0 calc(-1 * var(--s2));
120122 border-radius: 4px; border-left: 2px solid transparent; text-decoration: none; font-family: var(--mono); color: var(--ink);
121123 }
@@ -127,9 +129,11 @@
127129 & .st[data-st="A"] { color: var(--teal); }
128130 & .st[data-st="D"] { color: var(--red); }
129131 & .nm { overflow: hidden; white-space: nowrap; display: flex; min-width: 0; }
130- & .nm .p { flex: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
132+ /* the name gives way last: the directory shrinks first, down to its
133+ ellipsis, before the name is cut */
134+ & .nm .p { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
131135 /* cut from the start (rtl), the path itself kept in its order (bdi) */
132- & .nm .d { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; direction: rtl; text-align: left; color: var(--ink-mute); padding-left: 1ch; }
136+ & .nm .d { flex: 0 1000 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; direction: rtl; text-align: left; color: var(--ink-mute); padding-left: 1ch; }
133137 & .nm .d bdi { direction: ltr; }
134138 & .cn { font: 600 var(--fs-xs) / 16px var(--font); padding: 0 6px; border-radius: 999px; background: var(--ink-hi); color: var(--on-ink); align-self: center; }
135139 & .cn.done { background: var(--bg-strong); color: var(--ink-dim); }
@@ -148,18 +152,20 @@
148152 & .c-sha { width: 96px; }
149153 & .c-sub { width: auto; }
150154 }
151- /* the commit table inside a rail block: no frame of its own */
155+ /* the commit table inside a rail block: no frame of its own. The id, the
156+ checks and the date take the width of their text (an empty checks cell
157+ next to nothing); the subject takes the rest, cut at its end only. */
152158 .rail .list.commits {
153- border: 0; border-radius: 0; background: none;
154- & td { height: 34px; padding: 0 var(--s2); }
159+ border: 0; border-radius: 0; background: none; table-layout: auto;
160+ & td { height: 34px; padding: 0 var(--s2); width: 1%; white-space: nowrap; }
155161 & td.cur { width: 3px; padding: 0; }
156162 & td:nth-child(2) { padding-left: var(--s3); }
157163 & td:last-child { padding-right: var(--s4); }
158164 & .c-who { display: none; }
159- & .c-sha { width: 72px; }
160- & .c-subject { font-size: var(--fs-s); }
165+ & .c-subject { width: auto; max-width: 0; font-size: var(--fs-s); }
161166 & .c-subject .pill { display: none; }
162- & .c-when { width: 70px; text-align: right; }
167+ & .c-ck:empty { padding: 0; }
168+ & .c-when { text-align: right; }
163169 }
164170 .rail section > p { overflow-wrap: anywhere; }
165171 @media (max-width: 700px) {
@@ -115,7 +115,9 @@
115 font-size: var(--fs-s); position: relative; /* the offsets of its links count from it (ermite.js) */115 font-size: var(--fs-s); position: relative; /* the offsets of its links count from it (ermite.js) */
116 & li[hidden] { display: none; }116 & li[hidden] { display: none; }
117 & a {117 & a {
118- display: grid; grid-template-columns: 1.5ch minmax(0, 1fr) auto auto; column-gap: var(--s2); align-items: baseline;118+ /* centred, not on the baseline: the smaller counts on a baseline sit
119+ a pixel below the row, enough to give the list a scroll bar */
120+ display: grid; grid-template-columns: 1.5ch minmax(0, 1fr) auto auto; column-gap: var(--s2); align-items: center;
119 height: 26px; line-height: 26px; padding: 0 var(--s2); margin: 0 calc(-1 * var(--s2));121 height: 26px; line-height: 26px; padding: 0 var(--s2); margin: 0 calc(-1 * var(--s2));
120 border-radius: 4px; border-left: 2px solid transparent; text-decoration: none; font-family: var(--mono); color: var(--ink);122 border-radius: 4px; border-left: 2px solid transparent; text-decoration: none; font-family: var(--mono); color: var(--ink);
121 }123 }
@@ -127,9 +129,11 @@
127 & .st[data-st="A"] { color: var(--teal); }129 & .st[data-st="A"] { color: var(--teal); }
128 & .st[data-st="D"] { color: var(--red); }130 & .st[data-st="D"] { color: var(--red); }
129 & .nm { overflow: hidden; white-space: nowrap; display: flex; min-width: 0; }131 & .nm { overflow: hidden; white-space: nowrap; display: flex; min-width: 0; }
130- & .nm .p { flex: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }132+ /* the name gives way last: the directory shrinks first, down to its
133+ ellipsis, before the name is cut */
134+ & .nm .p { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
131 /* cut from the start (rtl), the path itself kept in its order (bdi) */135 /* cut from the start (rtl), the path itself kept in its order (bdi) */
132- & .nm .d { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; direction: rtl; text-align: left; color: var(--ink-mute); padding-left: 1ch; }136+ & .nm .d { flex: 0 1000 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; direction: rtl; text-align: left; color: var(--ink-mute); padding-left: 1ch; }
133 & .nm .d bdi { direction: ltr; }137 & .nm .d bdi { direction: ltr; }
134 & .cn { font: 600 var(--fs-xs) / 16px var(--font); padding: 0 6px; border-radius: 999px; background: var(--ink-hi); color: var(--on-ink); align-self: center; }138 & .cn { font: 600 var(--fs-xs) / 16px var(--font); padding: 0 6px; border-radius: 999px; background: var(--ink-hi); color: var(--on-ink); align-self: center; }
135 & .cn.done { background: var(--bg-strong); color: var(--ink-dim); }139 & .cn.done { background: var(--bg-strong); color: var(--ink-dim); }
@@ -148,18 +152,20 @@
148 & .c-sha { width: 96px; }152 & .c-sha { width: 96px; }
149 & .c-sub { width: auto; }153 & .c-sub { width: auto; }
150 }154 }
151- /* the commit table inside a rail block: no frame of its own */155+ /* the commit table inside a rail block: no frame of its own. The id, the
156+ checks and the date take the width of their text (an empty checks cell
157+ next to nothing); the subject takes the rest, cut at its end only. */
152 .rail .list.commits {158 .rail .list.commits {
153- border: 0; border-radius: 0; background: none;159+ border: 0; border-radius: 0; background: none; table-layout: auto;
154- & td { height: 34px; padding: 0 var(--s2); }160+ & td { height: 34px; padding: 0 var(--s2); width: 1%; white-space: nowrap; }
155 & td.cur { width: 3px; padding: 0; }161 & td.cur { width: 3px; padding: 0; }
156 & td:nth-child(2) { padding-left: var(--s3); }162 & td:nth-child(2) { padding-left: var(--s3); }
157 & td:last-child { padding-right: var(--s4); }163 & td:last-child { padding-right: var(--s4); }
158 & .c-who { display: none; }164 & .c-who { display: none; }
159- & .c-sha { width: 72px; }165+ & .c-subject { width: auto; max-width: 0; font-size: var(--fs-s); }
160- & .c-subject { font-size: var(--fs-s); }
161 & .c-subject .pill { display: none; }166 & .c-subject .pill { display: none; }
162- & .c-when { width: 70px; text-align: right; }167+ & .c-ck:empty { padding: 0; }
168+ & .c-when { text-align: right; }
163 }169 }
164 .rail section > p { overflow-wrap: anywhere; }170 .rail section > p { overflow-wrap: anywhere; }
165 @media (max-width: 700px) {171 @media (max-width: 700px) {
M internal/adapters/web/static/css/65-ci.css +8 -1
@@ -20,7 +20,8 @@
2020 the refs list has the mark next to the sha. */
2121 .c-ck { width: 1%; white-space: nowrap; }
2222 .list .c-ck { width: 92px; }
23- .rail .list.commits .c-ck { width: 76px; }
23+ /* the commit table of a rail lays out on its content (50-code.css) */
24+ .rail .list.commits .c-ck { width: 1%; }
2425 .list.refs .c-sha { width: 150px; }
2526
2627 /* the table is fixed: the status column is the icon, its margin and the
@@ -59,6 +60,12 @@
5960 .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); }
6061 .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); }
6162 .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); }
63+ /* the merge rule under the checks: a note about them, not one of them */
64+ .rail section > .rule-note {
65+ display: flex; gap: 6px; margin-top: var(--s3); padding-top: var(--s2); border-top: 1px dashed var(--border);
66+ font-size: var(--fs-xs); font-style: italic; line-height: 1.4; color: var(--ink-mute);
67+ &::before { content: "i"; flex: none; width: 14px; height: 14px; margin-top: 1px; border: 1px solid currentColor; border-radius: 50%; font: 700 9px / 12px var(--mono); font-style: normal; text-align: center; }
68+ }
6269
6370 /* annotations and summaries of a run */
6471 .annots { margin: 0 0 var(--s4); }
@@ -20,7 +20,8 @@
20 the refs list has the mark next to the sha. */20 the refs list has the mark next to the sha. */
21 .c-ck { width: 1%; white-space: nowrap; }21 .c-ck { width: 1%; white-space: nowrap; }
22 .list .c-ck { width: 92px; }22 .list .c-ck { width: 92px; }
23- .rail .list.commits .c-ck { width: 76px; }23+ /* the commit table of a rail lays out on its content (50-code.css) */
24+ .rail .list.commits .c-ck { width: 1%; }
24 .list.refs .c-sha { width: 150px; }25 .list.refs .c-sha { width: 150px; }
25 26
26 /* the table is fixed: the status column is the icon, its margin and the27 /* the table is fixed: the status column is the icon, its margin and the
@@ -59,6 +60,12 @@
59 .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); }60 .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); }
60 .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); }61 .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); }
61 .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); }62 .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); }
63+ /* the merge rule under the checks: a note about them, not one of them */
64+ .rail section > .rule-note {
65+ display: flex; gap: 6px; margin-top: var(--s3); padding-top: var(--s2); border-top: 1px dashed var(--border);
66+ font-size: var(--fs-xs); font-style: italic; line-height: 1.4; color: var(--ink-mute);
67+ &::before { content: "i"; flex: none; width: 14px; height: 14px; margin-top: 1px; border: 1px solid currentColor; border-radius: 50%; font: 700 9px / 12px var(--mono); font-style: normal; text-align: center; }
68+ }
62 69
63 /* annotations and summaries of a run */70 /* annotations and summaries of a run */
64 .annots { margin: 0 0 var(--s4); }71 .annots { margin: 0 0 var(--s4); }
M internal/app/testdata/web/summary.html +6 -11
@@ -147,20 +147,15 @@
147147 <dd>
148148 <time datetime="STAMP">STAMP</time>
149149 </dd>
150-</dl>
151-</section>
152-<section aria-labelledby="r-tag">
153-<h2 id="r-tag">Tags <span class="count">2</span>
154-<a href="/demo/refs">View all</a>
155-</h2>
156-<div class="latest-tag">
157-<a class="tag-name" href="/demo/tree/v0.2.0">
150+<dt>latest tag</dt>
151+<dd class="latest-tag">
152+<a class="tag-name" href="/demo/tree/v0.2.0" title="add main.go">
158153 <span class="ico i-tag" aria-hidden="true">
159154 </span>v0.2.0</a>
160-<span class="pill">latest</span>
161155 <time class="dim" datetime="STAMP" title="STAMP">AGO</time>
162-<p class="tag-sub dim">add main.go</p>
163-</div>
156+<a class="all-tags" href="/demo/refs">2 tags</a>
157+</dd>
158+</dl>
164159 </section>
165160 <section aria-labelledby="r-recent">
166161 <h2 id="r-recent">Recent commits <a href="/demo/commits/main">View all</a>
@@ -147,20 +147,15 @@
147 <dd>147 <dd>
148 <time datetime="STAMP">STAMP</time>148 <time datetime="STAMP">STAMP</time>
149 </dd>149 </dd>
150-</dl>150+<dt>latest tag</dt>
151-</section>151+<dd class="latest-tag">
152-<section aria-labelledby="r-tag">152+<a class="tag-name" href="/demo/tree/v0.2.0" title="add main.go">
153-<h2 id="r-tag">Tags <span class="count">2</span>
154-<a href="/demo/refs">View all</a>
155-</h2>
156-<div class="latest-tag">
157-<a class="tag-name" href="/demo/tree/v0.2.0">
158 <span class="ico i-tag" aria-hidden="true">153 <span class="ico i-tag" aria-hidden="true">
159 </span>v0.2.0</a>154 </span>v0.2.0</a>
160-<span class="pill">latest</span>
161 <time class="dim" datetime="STAMP" title="STAMP">AGO</time>155 <time class="dim" datetime="STAMP" title="STAMP">AGO</time>
162-<p class="tag-sub dim">add main.go</p>156+<a class="all-tags" href="/demo/refs">2 tags</a>
163-</div>157+</dd>
158+</dl>
164 </section>159 </section>
165 <section aria-labelledby="r-recent">160 <section aria-labelledby="r-recent">
166 <h2 id="r-recent">Recent commits <a href="/demo/commits/main">View all</a>161 <h2 id="r-recent">Recent commits <a href="/demo/commits/main">View all</a>
M internal/app/web_test.go +8 -6
@@ -264,14 +264,16 @@ func TestWebContentSecurity(t *testing.T) {
264264 }
265265 }
266266
267-// The summary rail names the newest tag; with more tags than it dates, it
268-// gives their count instead of an empty block.
267+// About, in the summary rail, names the newest tag; with more tags than it
268+// dates, it gives their count only.
269269 func TestWebSummaryLatestTag(t *testing.T) {
270270 h := newHarness(t, harnessOpts{})
271271 h.webRepo("demo")
272272 _, _, body := h.get("/demo")
273- assert.Contains(t, body, `<a class="tag-name" href="/demo/tree/v0.2.0">`)
274- assert.Contains(t, body, `<p class="tag-sub dim">add main.go</p>`)
273+ assert.Contains(t, body, `<dt>latest tag</dt>`)
274+ assert.Contains(t, body, `<a class="tag-name" href="/demo/tree/v0.2.0" title="add main.go">`)
275+ assert.Contains(t, body, `<a class="all-tags" href="/demo/refs">2 tags</a>`)
276+ assert.NotContains(t, body, `id="r-tag"`, "no Tags block of its own: the tag is a row of About")
275277
276278 h.seed("many", domain.VisibilityPublic, "a.txt")
277279 wt := t.TempDir()
@@ -281,6 +283,6 @@ func TestWebSummaryLatestTag(t *testing.T) {
281283 }
282284 h.mustGit(wt, "push", "-q", "--tags", "origin")
283285 _, _, body = h.get("/many")
284- assert.NotContains(t, body, `class="latest-tag"`)
285- assert.Contains(t, body, `<p class="dim">21 tags</p>`)
286+ assert.NotContains(t, body, `class="tag-name"`)
287+ assert.Contains(t, body, `<a class="all-tags" href="/many/refs">21 tags</a>`)
286288 }
@@ -264,14 +264,16 @@ func TestWebContentSecurity(t *testing.T) {
264 }264 }
265 }265 }
266 266
267-// The summary rail names the newest tag; with more tags than it dates, it267+// About, in the summary rail, names the newest tag; with more tags than it
268-// gives their count instead of an empty block.268+// dates, it gives their count only.
269 func TestWebSummaryLatestTag(t *testing.T) {269 func TestWebSummaryLatestTag(t *testing.T) {
270 h := newHarness(t, harnessOpts{})270 h := newHarness(t, harnessOpts{})
271 h.webRepo("demo")271 h.webRepo("demo")
272 _, _, body := h.get("/demo")272 _, _, body := h.get("/demo")
273- assert.Contains(t, body, `<a class="tag-name" href="/demo/tree/v0.2.0">`)273+ assert.Contains(t, body, `<dt>latest tag</dt>`)
274- assert.Contains(t, body, `<p class="tag-sub dim">add main.go</p>`)274+ assert.Contains(t, body, `<a class="tag-name" href="/demo/tree/v0.2.0" title="add main.go">`)
275+ assert.Contains(t, body, `<a class="all-tags" href="/demo/refs">2 tags</a>`)
276+ assert.NotContains(t, body, `id="r-tag"`, "no Tags block of its own: the tag is a row of About")
275 277
276 h.seed("many", domain.VisibilityPublic, "a.txt")278 h.seed("many", domain.VisibilityPublic, "a.txt")
277 wt := t.TempDir()279 wt := t.TempDir()
@@ -281,6 +283,6 @@ func TestWebSummaryLatestTag(t *testing.T) {
281 }283 }
282 h.mustGit(wt, "push", "-q", "--tags", "origin")284 h.mustGit(wt, "push", "-q", "--tags", "origin")
283 _, _, body = h.get("/many")285 _, _, body = h.get("/many")
284- assert.NotContains(t, body, `class="latest-tag"`)286+ assert.NotContains(t, body, `class="tag-name"`)
285- assert.Contains(t, body, `<p class="dim">21 tags</p>`)287+ assert.Contains(t, body, `<a class="all-tags" href="/many/refs">21 tags</a>`)
286 }288 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 44

↑↓ moveenter openesc close