detached 53e25a3

fix(web): keep the MR bar actions on the right and size the sticky offset from the bar

commit
53e25a360b92a093115c2051beb9c56d1afe48e6
author
brice · · 2026-10-21 23:41 UTC
committer
brice · · 2026-10-21 23:47 UTC
parent
5dbd27f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
8/8
refs
tag: v0.32.0
4 files · +94 -83
M internal/adapters/web/mr.templ +7 -6
@@ -467,12 +467,13 @@ templ mrBar(p MRPage) {
467467 <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge">
468468 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
469469 @diffViewMenu(p.DiffView)
470- <span class="grow"></span>
471- @MRChecksSum(p.ChecksSum)
472- if p.Box != nil {
473- @mergeMenu(p)
474- }
475- @moreMenu(p)
470+ <div class="mr-acts">
471+ @MRChecksSum(p.ChecksSum)
472+ if p.Box != nil {
473+ @mergeMenu(p)
474+ }
475+ @moreMenu(p)
476+ </div>
476477 </div>
477478 }
478479
@@ -467,12 +467,13 @@ templ mrBar(p MRPage) {
467 <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge">467 <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge">
468 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>468 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
469 @diffViewMenu(p.DiffView)469 @diffViewMenu(p.DiffView)
470- <span class="grow"></span>470+ <div class="mr-acts">
471- @MRChecksSum(p.ChecksSum)471+ @MRChecksSum(p.ChecksSum)
472- if p.Box != nil {472+ if p.Box != nil {
473- @mergeMenu(p)473+ @mergeMenu(p)
474- }474+ }
475- @moreMenu(p)475+ @moreMenu(p)
476+ </div>
476 </div>477 </div>
477 }478 }
478 479
M internal/adapters/web/mr_templ.go +71 -71 generated file: collapsed
M internal/adapters/web/static/css/60-mr.css +4 -6
@@ -61,14 +61,12 @@
6161
6262 /* the bar above the diff of a merge request: the diff tools, the checks and
6363 the menus of the owner, on one line that follows the review */
64- /* on a narrow screen the bar takes two lines: the diff tools, then the
65- checks and the menus; the file headers stick under both */
66- @media (max-width: 860px) {
67- body:has(.mr-bar) { --bar-h: 96px; }
68- .mr-bar .grow { flex-basis: 100%; height: 0; }
69- }
64+ /* the checks and the menus stay on the right, also when they wrap under the
65+ diff tools; mr.js measures the height of the bar (--mr-bar-h) */
66+ body:has(.mr-bar) { --bar-h: var(--mr-bar-h, 48px); }
7067 .mr-bar {
7168 gap: var(--s2);
69+ & .mr-acts { display: flex; gap: var(--s2); align-items: center; margin-left: auto; }
7270 & .mr-ck { margin-left: 0; height: 32px; padding: 0 var(--s2); border-radius: var(--r-s); text-decoration: none; color: var(--ink-dim); font-size: var(--fs-s); }
7371 & .mr-ck:hover { background: var(--bg-raised); color: var(--ink-hi); }
7472 }
@@ -61,14 +61,12 @@
61 61
62 /* the bar above the diff of a merge request: the diff tools, the checks and62 /* the bar above the diff of a merge request: the diff tools, the checks and
63 the menus of the owner, on one line that follows the review */63 the menus of the owner, on one line that follows the review */
64- /* on a narrow screen the bar takes two lines: the diff tools, then the64+ /* the checks and the menus stay on the right, also when they wrap under the
65- checks and the menus; the file headers stick under both */65+ diff tools; mr.js measures the height of the bar (--mr-bar-h) */
66- @media (max-width: 860px) {66+ body:has(.mr-bar) { --bar-h: var(--mr-bar-h, 48px); }
67- body:has(.mr-bar) { --bar-h: 96px; }
68- .mr-bar .grow { flex-basis: 100%; height: 0; }
69- }
70 .mr-bar {67 .mr-bar {
71 gap: var(--s2);68 gap: var(--s2);
69+ & .mr-acts { display: flex; gap: var(--s2); align-items: center; margin-left: auto; }
72 & .mr-ck { margin-left: 0; height: 32px; padding: 0 var(--s2); border-radius: var(--r-s); text-decoration: none; color: var(--ink-dim); font-size: var(--fs-s); }70 & .mr-ck { margin-left: 0; height: 32px; padding: 0 var(--s2); border-radius: var(--r-s); text-decoration: none; color: var(--ink-dim); font-size: var(--fs-s); }
73 & .mr-ck:hover { background: var(--bg-raised); color: var(--ink-hi); }71 & .mr-ck:hover { background: var(--bg-raised); color: var(--ink-hi); }
74 }72 }
M internal/adapters/web/static/js/mr.js +12 -0
@@ -81,6 +81,16 @@ window.ermiteMR = (() => {
8181 const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
8282 let refreshTouchComments = () => {};
8383 let revealGen = 0; // the last reveal wins the scroll
84+ // The bar wraps only when its items do not fit: the file headers stick
85+ // under its measured height (--mr-bar-h), not under a guessed one.
86+ let barObserver = null;
87+ function watchBar() {
88+ barObserver?.disconnect();
89+ const bar = $('.mr-bar');
90+ if (!bar || typeof ResizeObserver !== 'function') return;
91+ barObserver = new ResizeObserver(() => document.body.style.setProperty('--mr-bar-h', bar.offsetHeight + 'px'));
92+ barObserver.observe(bar);
93+ }
8494 const load = (ph) => window.ermiteLazy.load(ph);
8595 window.ermiteLazy.onSwap(() => refreshTouchComments());
8696
@@ -536,6 +546,7 @@ window.ermiteMR = (() => {
536546 if (oldSHA && newSHA) oldSHA.textContent = newSHA.textContent;
537547 page = null;
538548 panels(openPanel);
549+ watchBar();
539550 foldFiles(!!folded);
540551 if ($('.rail')) $('.rail').scrollTop = railY;
541552 if ($('.r-files .files')) $('.r-files .files').scrollTop = filesY;
@@ -593,6 +604,7 @@ window.ermiteMR = (() => {
593604 headRefresh.resume();
594605 });
595606 panels();
607+ watchBar();
596608 // Large diffs may contain only the split table. Ask the existing server
597609 // route for unified rather than writing the saved session preference.
598610 const narrow = window.matchMedia?.('(max-width: 720px)');
@@ -81,6 +81,16 @@ window.ermiteMR = (() => {
81 const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];81 const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
82 let refreshTouchComments = () => {};82 let refreshTouchComments = () => {};
83 let revealGen = 0; // the last reveal wins the scroll83 let revealGen = 0; // the last reveal wins the scroll
84+ // The bar wraps only when its items do not fit: the file headers stick
85+ // under its measured height (--mr-bar-h), not under a guessed one.
86+ let barObserver = null;
87+ function watchBar() {
88+ barObserver?.disconnect();
89+ const bar = $('.mr-bar');
90+ if (!bar || typeof ResizeObserver !== 'function') return;
91+ barObserver = new ResizeObserver(() => document.body.style.setProperty('--mr-bar-h', bar.offsetHeight + 'px'));
92+ barObserver.observe(bar);
93+ }
84 const load = (ph) => window.ermiteLazy.load(ph);94 const load = (ph) => window.ermiteLazy.load(ph);
85 window.ermiteLazy.onSwap(() => refreshTouchComments());95 window.ermiteLazy.onSwap(() => refreshTouchComments());
86 96
@@ -536,6 +546,7 @@ window.ermiteMR = (() => {
536 if (oldSHA && newSHA) oldSHA.textContent = newSHA.textContent;546 if (oldSHA && newSHA) oldSHA.textContent = newSHA.textContent;
537 page = null;547 page = null;
538 panels(openPanel);548 panels(openPanel);
549+ watchBar();
539 foldFiles(!!folded);550 foldFiles(!!folded);
540 if ($('.rail')) $('.rail').scrollTop = railY;551 if ($('.rail')) $('.rail').scrollTop = railY;
541 if ($('.r-files .files')) $('.r-files .files').scrollTop = filesY;552 if ($('.r-files .files')) $('.r-files .files').scrollTop = filesY;
@@ -593,6 +604,7 @@ window.ermiteMR = (() => {
593 headRefresh.resume();604 headRefresh.resume();
594 });605 });
595 panels();606 panels();
607+ watchBar();
596 // Large diffs may contain only the split table. Ask the existing server608 // Large diffs may contain only the split table. Ask the existing server
597 // route for unified rather than writing the saved session preference.609 // route for unified rather than writing the saved session preference.
598 const narrow = window.matchMedia?.('(max-width: 720px)');610 const narrow = window.matchMedia?.('(max-width: 720px)');

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 42

↑↓ moveenter openesc close