detached 5e5d8fb

fix(web): reopen diff files on a layout switch and let x expand them again

commit
5e5d8fbe2752b45a31f22547f931af6c206f0559
author
brice · · 2026-10-11 12:46 UTC
parent
e84f160
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +319 -268
M internal/adapters/web/diff.templ +9 -1
@@ -55,7 +55,7 @@ templ Commit(l Layout, p CommitPage) {
5555 <span class="grow"></span>
5656 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
5757 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
58- <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd aria-hidden="true">x</kbd></button>
58+ @collapseButton()
5959 </div>
6060 if p.Invisible {
6161 <p class="notice">
@@ -112,6 +112,14 @@ templ viewLink(p DiffView, view, label, key, href string) {
112112 >{ label }<kbd aria-hidden="true">{ key }</kbd></a>
113113 }
114114
115+// collapseButton folds every file of the diff, or opens them all again when
116+// none is open; its label follows the state.
117+templ collapseButton() {
118+ <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">
119+ <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span><kbd aria-hidden="true">x</kbd>
120+ </button>
121+}
122+
115123 templ personView(pr Person) {
116124 { pr.Name }
117125 if pr.WhenAt != "" {
@@ -55,7 +55,7 @@ templ Commit(l Layout, p CommitPage) {
55 <span class="grow"></span>55 <span class="grow"></span>
56 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)56 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
57 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)57 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
58- <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd aria-hidden="true">x</kbd></button>58+ @collapseButton()
59 </div>59 </div>
60 if p.Invisible {60 if p.Invisible {
61 <p class="notice">61 <p class="notice">
@@ -112,6 +112,14 @@ templ viewLink(p DiffView, view, label, key, href string) {
112 >{ label }<kbd aria-hidden="true">{ key }</kbd></a>112 >{ label }<kbd aria-hidden="true">{ key }</kbd></a>
113 }113 }
114 114
115+// collapseButton folds every file of the diff, or opens them all again when
116+// none is open; its label follows the state.
117+templ collapseButton() {
118+ <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">
119+ <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span><kbd aria-hidden="true">x</kbd>
120+ </button>
121+}
122+
115 templ personView(pr Person) {123 templ personView(pr Person) {
116 { pr.Name }124 { pr.Name }
117 if pr.WhenAt != "" {125 if pr.WhenAt != "" {
M internal/adapters/web/diff_templ.go +292 -257 generated file: collapsed
M internal/adapters/web/keys.templ +3 -3
@@ -52,9 +52,9 @@ templ Keys() {
5252 let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3);
5353 let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' })
5454 };
55- k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split');
56- k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified');
57- k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false);
55+ k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); q('[data-file]').arr().forEach(d => d.open = true);
56+ k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); q('[data-file]').arr().forEach(d => d.open = true);
57+ k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) };
5858 k-permalink from:body -> {
5959 let b = document.body.dataset;
6060 if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return }
@@ -52,9 +52,9 @@ templ Keys() {
52 let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3);52 let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3);
53 let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' })53 let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' })
54 };54 };
55- k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split');55+ k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); q('[data-file]').arr().forEach(d => d.open = true);
56- k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified');56+ k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); q('[data-file]').arr().forEach(d => d.open = true);
57- k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false);57+ k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) };
58 k-permalink from:body -> {58 k-permalink from:body -> {
59 let b = document.body.dataset;59 let b = document.body.dataset;
60 if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return }60 if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return }
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/mr.templ +2 -2
@@ -168,7 +168,7 @@ templ MergeRequest(l Layout, p MRPage) {
168168 <span class="grow"></span>
169169 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
170170 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
171- <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd aria-hidden="true">x</kbd></button>
171+ @collapseButton()
172172 </div>
173173 if p.Invisible {
174174 <p class="notice">
@@ -448,7 +448,7 @@ templ Compare(l Layout, p ComparePage) {
448448 <span class="grow"></span>
449449 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
450450 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
451- <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd aria-hidden="true">x</kbd></button>
451+ @collapseButton()
452452 </div>
453453 if p.Invisible {
454454 <p class="notice">
@@ -168,7 +168,7 @@ templ MergeRequest(l Layout, p MRPage) {
168 <span class="grow"></span>168 <span class="grow"></span>
169 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)169 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
170 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)170 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
171- <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd aria-hidden="true">x</kbd></button>171+ @collapseButton()
172 </div>172 </div>
173 if p.Invisible {173 if p.Invisible {
174 <p class="notice">174 <p class="notice">
@@ -448,7 +448,7 @@ templ Compare(l Layout, p ComparePage) {
448 <span class="grow"></span>448 <span class="grow"></span>
449 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)449 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
450 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)450 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
451- <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd aria-hidden="true">x</kbd></button>451+ @collapseButton()
452 </div>452 </div>
453 if p.Invisible {453 if p.Invisible {
454 <p class="notice">454 <p class="notice">
M internal/adapters/web/mr_templ.go +10 -2
@@ -974,7 +974,11 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
974974 if templ_7745c5c3_Err != nil {
975975 return templ_7745c5c3_Err
976976 }
977- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<button type=\"button\" aria-keyshortcuts=\"x\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-collapse'))\" class=\"btn\">Collapse all<kbd aria-hidden=\"true\">x</kbd></button></div>")
977+ templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer)
978+ if templ_7745c5c3_Err != nil {
979+ return templ_7745c5c3_Err
980+ }
981+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</div>")
978982 if templ_7745c5c3_Err != nil {
979983 return templ_7745c5c3_Err
980984 }
@@ -2444,7 +2448,11 @@ func Compare(l Layout, p ComparePage) templ.Component {
24442448 if templ_7745c5c3_Err != nil {
24452449 return templ_7745c5c3_Err
24462450 }
2447- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "<button type=\"button\" aria-keyshortcuts=\"x\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-collapse'))\" class=\"btn\">Collapse all<kbd aria-hidden=\"true\">x</kbd></button></div>")
2451+ templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer)
2452+ if templ_7745c5c3_Err != nil {
2453+ return templ_7745c5c3_Err
2454+ }
2455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "</div>")
24482456 if templ_7745c5c3_Err != nil {
24492457 return templ_7745c5c3_Err
24502458 }
@@ -974,7 +974,11 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
974 if templ_7745c5c3_Err != nil {974 if templ_7745c5c3_Err != nil {
975 return templ_7745c5c3_Err975 return templ_7745c5c3_Err
976 }976 }
977- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<button type=\"button\" aria-keyshortcuts=\"x\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-collapse'))\" class=\"btn\">Collapse all<kbd aria-hidden=\"true\">x</kbd></button></div>")977+ templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer)
978+ if templ_7745c5c3_Err != nil {
979+ return templ_7745c5c3_Err
980+ }
981+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</div>")
978 if templ_7745c5c3_Err != nil {982 if templ_7745c5c3_Err != nil {
979 return templ_7745c5c3_Err983 return templ_7745c5c3_Err
980 }984 }
@@ -2444,7 +2448,11 @@ func Compare(l Layout, p ComparePage) templ.Component {
2444 if templ_7745c5c3_Err != nil {2448 if templ_7745c5c3_Err != nil {
2445 return templ_7745c5c3_Err2449 return templ_7745c5c3_Err
2446 }2450 }
2447- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "<button type=\"button\" aria-keyshortcuts=\"x\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-collapse'))\" class=\"btn\">Collapse all<kbd aria-hidden=\"true\">x</kbd></button></div>")2451+ templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer)
2452+ if templ_7745c5c3_Err != nil {
2453+ return templ_7745c5c3_Err
2454+ }
2455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "</div>")
2448 if templ_7745c5c3_Err != nil {2456 if templ_7745c5c3_Err != nil {
2449 return templ_7745c5c3_Err2457 return templ_7745c5c3_Err
2450 }2458 }
M internal/adapters/web/overlays.templ +1 -1
@@ -19,7 +19,7 @@ templ OverlayHelp() {
1919 <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd></div>
2020 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>
2121 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>
22- <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse all</dd></div>
22+ <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div>
2323 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>
2424 <div><dt><kbd>esc</kbd></dt><dd>close a dialog, leave a field</dd></div>
2525 </dl>
@@ -19,7 +19,7 @@ templ OverlayHelp() {
19 <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd></div>19 <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd></div>
20 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>20 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>
21 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>21 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>
22- <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse all</dd></div>22+ <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div>
23 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>23 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>
24 <div><dt><kbd>esc</kbd></dt><dd>close a dialog, leave a field</dd></div>24 <div><dt><kbd>esc</kbd></dt><dd>close a dialog, leave a field</dd></div>
25 </dl>25 </dl>
M internal/adapters/web/overlays_templ.go +1 -1 generated file: collapsed

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 45

↑↓ moveenter openesc close