detached 55a688e

feat(web): h/l move between the content and the side panel (#49)

commit
55a688e4d192f040fba9fc6c0a70a78b0d4922e4
author
brice · · 2026-10-17 22:40 UTC
committer
bnema · · 2026-10-17 22:40 UTC
parent
126ed2b
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
refs
tag: v0.19.0
37 files · +426 -15
A internal/adapters/web/jstest/rail.test.mjs +64 -0
@@ -0,0 +1,64 @@
1+// Tests of the side panel keys of static/js/ermite.js (ermite.rail, and j / k
2+// inside the panel) on a tiny hand-rolled DOM. Run with
3+// node --test 'internal/adapters/web/jstest/*.test.mjs'
4+import assert from 'node:assert/strict';
5+import { readFileSync } from 'node:fs';
6+import { test } from 'node:test';
7+import { el, installDom } from './dom.mjs';
8+
9+// A page: <main>, and a rail with a hidden input, a closed-details command
10+// (not laid out), then the controls a key can reach.
11+const doc = installDom();
12+const main = el('main', { id: 'main' });
13+const hidden = el('input', { attrs: { type: 'hidden' } });
14+const folded = el('a', { href: '/x', attrs: { href: '/x' } });
15+folded.getClientRects = () => []; // inside a closed <details>
16+const markRead = el('button', { cls: 'btn' });
17+const ready = el('button', { cls: 'btn primary' });
18+const close = el('button', { cls: 'btn danger' });
19+const rail = el('aside', { cls: 'rail' }, hidden, folded, markRead, ready, close);
20+document.body.append(main, rail);
21+let active = document.body;
22+Object.defineProperty(doc, 'activeElement', { get: () => active });
23+for (const e of [main, markRead, ready, close, folded]) e.focus = () => { active = e; };
24+rail.contains = (e) => { for (let n = e; n; n = n.parent) if (n === rail) return true; return false; };
25+
26+// Only the ermiteRail block of the file is evaluated: the rest needs a page.
27+const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
28+const start = src.indexOf(' window.ermiteRail = ');
29+const end = src.indexOf(' })();', start) + ' })();'.length;
30+new Function(src.slice(start, end))();
31+const R = window.ermiteRail;
32+const E = { rail: (into) => (into ? R.enter() : R.leave()), move: (d) => R.step(d) };
33+
34+test('l focuses the main action of the panel, j / k walk its shown controls, h goes back', () => {
35+ active = main;
36+ E.rail(true);
37+ assert.equal(active, ready, 'the primary button first');
38+ E.move(1);
39+ assert.equal(active, close);
40+ E.move(1);
41+ assert.equal(active, close, 'stops at the last control');
42+ E.move(-1);
43+ E.move(-1);
44+ assert.equal(active, markRead, 'the folded link is skipped');
45+ E.move(-1);
46+ assert.equal(active, markRead, 'stops at the first shown control');
47+ E.rail(false);
48+ assert.equal(active, main);
49+});
50+
51+test('without a primary action, l takes the first shown control', () => {
52+ ready.className = 'btn';
53+ active = main;
54+ E.rail(true);
55+ assert.equal(active, markRead);
56+ ready.className = 'btn primary';
57+});
58+
59+test('h outside the panel leaves the focus alone; j / k outside it are not taken', () => {
60+ active = main;
61+ E.rail(false);
62+ assert.equal(active, main);
63+ assert.equal(R.step(1), false, 'the row cursor keeps j / k');
64+});
@@ -0,0 +1,64 @@
1+// Tests of the side panel keys of static/js/ermite.js (ermite.rail, and j / k
2+// inside the panel) on a tiny hand-rolled DOM. Run with
3+// node --test 'internal/adapters/web/jstest/*.test.mjs'
4+import assert from 'node:assert/strict';
5+import { readFileSync } from 'node:fs';
6+import { test } from 'node:test';
7+import { el, installDom } from './dom.mjs';
8+
9+// A page: <main>, and a rail with a hidden input, a closed-details command
10+// (not laid out), then the controls a key can reach.
11+const doc = installDom();
12+const main = el('main', { id: 'main' });
13+const hidden = el('input', { attrs: { type: 'hidden' } });
14+const folded = el('a', { href: '/x', attrs: { href: '/x' } });
15+folded.getClientRects = () => []; // inside a closed <details>
16+const markRead = el('button', { cls: 'btn' });
17+const ready = el('button', { cls: 'btn primary' });
18+const close = el('button', { cls: 'btn danger' });
19+const rail = el('aside', { cls: 'rail' }, hidden, folded, markRead, ready, close);
20+document.body.append(main, rail);
21+let active = document.body;
22+Object.defineProperty(doc, 'activeElement', { get: () => active });
23+for (const e of [main, markRead, ready, close, folded]) e.focus = () => { active = e; };
24+rail.contains = (e) => { for (let n = e; n; n = n.parent) if (n === rail) return true; return false; };
25+
26+// Only the ermiteRail block of the file is evaluated: the rest needs a page.
27+const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
28+const start = src.indexOf(' window.ermiteRail = ');
29+const end = src.indexOf(' })();', start) + ' })();'.length;
30+new Function(src.slice(start, end))();
31+const R = window.ermiteRail;
32+const E = { rail: (into) => (into ? R.enter() : R.leave()), move: (d) => R.step(d) };
33+
34+test('l focuses the main action of the panel, j / k walk its shown controls, h goes back', () => {
35+ active = main;
36+ E.rail(true);
37+ assert.equal(active, ready, 'the primary button first');
38+ E.move(1);
39+ assert.equal(active, close);
40+ E.move(1);
41+ assert.equal(active, close, 'stops at the last control');
42+ E.move(-1);
43+ E.move(-1);
44+ assert.equal(active, markRead, 'the folded link is skipped');
45+ E.move(-1);
46+ assert.equal(active, markRead, 'stops at the first shown control');
47+ E.rail(false);
48+ assert.equal(active, main);
49+});
50+
51+test('without a primary action, l takes the first shown control', () => {
52+ ready.className = 'btn';
53+ active = main;
54+ E.rail(true);
55+ assert.equal(active, markRead);
56+ ready.className = 'btn primary';
57+});
58+
59+test('h outside the panel leaves the focus alone; j / k outside it are not taken', () => {
60+ active = main;
61+ E.rail(false);
62+ assert.equal(active, main);
63+ assert.equal(R.step(1), false, 'the row cursor keeps j / k');
64+});
M internal/adapters/web/keys.templ +2 -0
@@ -14,6 +14,8 @@ templ Keys() {
1414 k-top from:body -> ermite.edge(false);
1515 k-bottom from:body -> ermite.edge(true);
1616 k-open from:body -> ermite.open();
17+ k-right from:body -> ermite.rail(true);
18+ k-left from:body -> ermite.rail(false);
1719 k-esc from:body -> ermite.closeDialogs();
1820 k-help from:body -> ermite.dialog('panel-help');
1921 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -14,6 +14,8 @@ templ Keys() {
14 k-top from:body -> ermite.edge(false);14 k-top from:body -> ermite.edge(false);
15 k-bottom from:body -> ermite.edge(true);15 k-bottom from:body -> ermite.edge(true);
16 k-open from:body -> ermite.open();16 k-open from:body -> ermite.open();
17+ k-right from:body -> ermite.rail(true);
18+ k-left from:body -> ermite.rail(false);
17 k-esc from:body -> ermite.closeDialogs();19 k-esc from:body -> ermite.closeDialogs();
18 k-help from:body -> ermite.dialog('panel-help');20 k-help from:body -> ermite.dialog('panel-help');
19 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');21 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/overlays.templ +1 -0
@@ -14,6 +14,7 @@ templ OverlayHelp(mr bool) {
1414 <div><dt><kbd>?</kbd></dt><dd>this list</dd></div>
1515 <div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move the row cursor down / up</dd></div>
1616 <div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor</dd></div>
17+ <div><dt><kbd>l</kbd> <kbd>h</kbd></dt><dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd></div>
1718 <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div>
1819 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file in the repository</dd></div>
1920 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>
@@ -14,6 +14,7 @@ templ OverlayHelp(mr bool) {
14 <div><dt><kbd>?</kbd></dt><dd>this list</dd></div>14 <div><dt><kbd>?</kbd></dt><dd>this list</dd></div>
15 <div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move the row cursor down / up</dd></div>15 <div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move the row cursor down / up</dd></div>
16 <div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor</dd></div>16 <div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor</dd></div>
17+ <div><dt><kbd>l</kbd> <kbd>h</kbd></dt><dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd></div>
17 <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div>18 <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div>
18 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file in the repository</dd></div>19 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file in the repository</dd></div>
19 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>20 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>
M internal/adapters/web/overlays_templ.go +10 -10
@@ -35,14 +35,14 @@ func OverlayHelp(mr bool) templ.Component {
3535 templ_7745c5c3_Var1 = templ.NopComponent
3636 }
3737 ctx = templ.ClearChildren(ctx)
38- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<dialog id=\"panel-help\" class=\"panel modal\" aria-label=\"Keyboard shortcuts\" hx-on:keydown=\"event.key === 'Escape' && (event.preventDefault(), this.close())\" hx-on:click=\"event.target === this && this.close()\"><div class=\"panel-head\"><h2>Keyboard shortcuts</h2><button type=\"button\" class=\"panel-x\" aria-label=\"Close\" hx-on:click=\"closest('dialog').close()\"><kbd>esc</kbd> close</button></div><dl class=\"keys\"><div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div><div><dt><kbd>?</kbd></dt><dd>this list</dd></div><div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move the row cursor down / up</dd></div><div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor</dd></div><div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div><div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file in the repository</dd></div><div><dt><kbd>b</kbd></dt><dd>")
38+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<dialog id=\"panel-help\" class=\"panel modal\" aria-label=\"Keyboard shortcuts\" hx-on:keydown=\"event.key === 'Escape' && (event.preventDefault(), this.close())\" hx-on:click=\"event.target === this && this.close()\"><div class=\"panel-head\"><h2>Keyboard shortcuts</h2><button type=\"button\" class=\"panel-x\" aria-label=\"Close\" hx-on:click=\"closest('dialog').close()\"><kbd>esc</kbd> close</button></div><dl class=\"keys\"><div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div><div><dt><kbd>?</kbd></dt><dd>this list</dd></div><div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move the row cursor down / up</dd></div><div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor</dd></div><div><dt><kbd>l</kbd> <kbd>h</kbd></dt><dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd></div><div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div><div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file in the repository</dd></div><div><dt><kbd>b</kbd></dt><dd>")
3939 if templ_7745c5c3_Err != nil {
4040 return templ_7745c5c3_Err
4141 }
4242 var templ_7745c5c3_Var2 string
4343 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("switch")
4444 if templ_7745c5c3_Err != nil {
45- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 19, Col: 43}
45+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 20, Col: 43}
4646 }
4747 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
4848 if templ_7745c5c3_Err != nil {
@@ -96,7 +96,7 @@ func OverlayRefs(l Layout) templ.Component {
9696 var templ_7745c5c3_Var4 string
9797 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(l.Heads)))
9898 if templ_7745c5c3_Err != nil {
99- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 53, Col: 58}
99+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 54, Col: 58}
100100 }
101101 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
102102 if templ_7745c5c3_Err != nil {
@@ -119,7 +119,7 @@ func OverlayRefs(l Layout) templ.Component {
119119 var templ_7745c5c3_Var5 string
120120 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(l.Tags)))
121121 if templ_7745c5c3_Err != nil {
122- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 61, Col: 53}
122+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 62, Col: 53}
123123 }
124124 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
125125 if templ_7745c5c3_Err != nil {
@@ -172,7 +172,7 @@ func refItem(r RefItem) templ.Component {
172172 var templ_7745c5c3_Var7 templ.SafeURL
173173 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
174174 if templ_7745c5c3_Err != nil {
175- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 76, Col: 31}
175+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 77, Col: 31}
176176 }
177177 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
178178 if templ_7745c5c3_Err != nil {
@@ -210,7 +210,7 @@ func refItem(r RefItem) templ.Component {
210210 var templ_7745c5c3_Var8 string
211211 templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
212212 if templ_7745c5c3_Err != nil {
213- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 87, Col: 18}
213+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 88, Col: 18}
214214 }
215215 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
216216 if templ_7745c5c3_Err != nil {
@@ -259,7 +259,7 @@ func OverlayCmd(items []CmdItem) templ.Component {
259259 var templ_7745c5c3_Var10 string
260260 templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(strings.ToLower(c.Kind + " " + c.Label))
261261 if templ_7745c5c3_Err != nil {
262- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 112, Col: 56}
262+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 113, Col: 56}
263263 }
264264 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
265265 if templ_7745c5c3_Err != nil {
@@ -282,7 +282,7 @@ func OverlayCmd(items []CmdItem) templ.Component {
282282 var templ_7745c5c3_Var11 templ.SafeURL
283283 templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
284284 if templ_7745c5c3_Err != nil {
285- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 112, Col: 114}
285+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 113, Col: 114}
286286 }
287287 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
288288 if templ_7745c5c3_Err != nil {
@@ -295,7 +295,7 @@ func OverlayCmd(items []CmdItem) templ.Component {
295295 var templ_7745c5c3_Var12 string
296296 templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
297297 if templ_7745c5c3_Err != nil {
298- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 112, Col: 158}
298+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 113, Col: 158}
299299 }
300300 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
301301 if templ_7745c5c3_Err != nil {
@@ -308,7 +308,7 @@ func OverlayCmd(items []CmdItem) templ.Component {
308308 var templ_7745c5c3_Var13 string
309309 templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(c.Kind)
310310 if templ_7745c5c3_Err != nil {
311- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 112, Col: 191}
311+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 113, Col: 191}
312312 }
313313 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
314314 if templ_7745c5c3_Err != nil {
@@ -35,14 +35,14 @@ func OverlayHelp(mr bool) templ.Component {
35 templ_7745c5c3_Var1 = templ.NopComponent35 templ_7745c5c3_Var1 = templ.NopComponent
36 }36 }
37 ctx = templ.ClearChildren(ctx)37 ctx = templ.ClearChildren(ctx)
38- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<dialog id=\"panel-help\" class=\"panel modal\" aria-label=\"Keyboard shortcuts\" hx-on:keydown=\"event.key === 'Escape' && (event.preventDefault(), this.close())\" hx-on:click=\"event.target === this && this.close()\"><div class=\"panel-head\"><h2>Keyboard shortcuts</h2><button type=\"button\" class=\"panel-x\" aria-label=\"Close\" hx-on:click=\"closest('dialog').close()\"><kbd>esc</kbd> close</button></div><dl class=\"keys\"><div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div><div><dt><kbd>?</kbd></dt><dd>this list</dd></div><div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move the row cursor down / up</dd></div><div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor</dd></div><div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div><div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file in the repository</dd></div><div><dt><kbd>b</kbd></dt><dd>")38+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<dialog id=\"panel-help\" class=\"panel modal\" aria-label=\"Keyboard shortcuts\" hx-on:keydown=\"event.key === 'Escape' && (event.preventDefault(), this.close())\" hx-on:click=\"event.target === this && this.close()\"><div class=\"panel-head\"><h2>Keyboard shortcuts</h2><button type=\"button\" class=\"panel-x\" aria-label=\"Close\" hx-on:click=\"closest('dialog').close()\"><kbd>esc</kbd> close</button></div><dl class=\"keys\"><div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div><div><dt><kbd>?</kbd></dt><dd>this list</dd></div><div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move the row cursor down / up</dd></div><div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor</dd></div><div><dt><kbd>l</kbd> <kbd>h</kbd></dt><dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd></div><div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div><div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file in the repository</dd></div><div><dt><kbd>b</kbd></dt><dd>")
39 if templ_7745c5c3_Err != nil {39 if templ_7745c5c3_Err != nil {
40 return templ_7745c5c3_Err40 return templ_7745c5c3_Err
41 }41 }
42 var templ_7745c5c3_Var2 string42 var templ_7745c5c3_Var2 string
43 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("switch")43 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("switch")
44 if templ_7745c5c3_Err != nil {44 if templ_7745c5c3_Err != nil {
45- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 19, Col: 43}45+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 20, Col: 43}
46 }46 }
47 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))47 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
48 if templ_7745c5c3_Err != nil {48 if templ_7745c5c3_Err != nil {
@@ -96,7 +96,7 @@ func OverlayRefs(l Layout) templ.Component {
96 var templ_7745c5c3_Var4 string96 var templ_7745c5c3_Var4 string
97 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(l.Heads)))97 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(l.Heads)))
98 if templ_7745c5c3_Err != nil {98 if templ_7745c5c3_Err != nil {
99- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 53, Col: 58}99+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 54, Col: 58}
100 }100 }
101 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))101 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
102 if templ_7745c5c3_Err != nil {102 if templ_7745c5c3_Err != nil {
@@ -119,7 +119,7 @@ func OverlayRefs(l Layout) templ.Component {
119 var templ_7745c5c3_Var5 string119 var templ_7745c5c3_Var5 string
120 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(l.Tags)))120 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(l.Tags)))
121 if templ_7745c5c3_Err != nil {121 if templ_7745c5c3_Err != nil {
122- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 61, Col: 53}122+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 62, Col: 53}
123 }123 }
124 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))124 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
125 if templ_7745c5c3_Err != nil {125 if templ_7745c5c3_Err != nil {
@@ -172,7 +172,7 @@ func refItem(r RefItem) templ.Component {
172 var templ_7745c5c3_Var7 templ.SafeURL172 var templ_7745c5c3_Var7 templ.SafeURL
173 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))173 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
174 if templ_7745c5c3_Err != nil {174 if templ_7745c5c3_Err != nil {
175- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 76, Col: 31}175+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 77, Col: 31}
176 }176 }
177 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))177 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
178 if templ_7745c5c3_Err != nil {178 if templ_7745c5c3_Err != nil {
@@ -210,7 +210,7 @@ func refItem(r RefItem) templ.Component {
210 var templ_7745c5c3_Var8 string210 var templ_7745c5c3_Var8 string
211 templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)211 templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
212 if templ_7745c5c3_Err != nil {212 if templ_7745c5c3_Err != nil {
213- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 87, Col: 18}213+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 88, Col: 18}
214 }214 }
215 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))215 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
216 if templ_7745c5c3_Err != nil {216 if templ_7745c5c3_Err != nil {
@@ -259,7 +259,7 @@ func OverlayCmd(items []CmdItem) templ.Component {
259 var templ_7745c5c3_Var10 string259 var templ_7745c5c3_Var10 string
260 templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(strings.ToLower(c.Kind + " " + c.Label))260 templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(strings.ToLower(c.Kind + " " + c.Label))
261 if templ_7745c5c3_Err != nil {261 if templ_7745c5c3_Err != nil {
262- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 112, Col: 56}262+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 113, Col: 56}
263 }263 }
264 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)264 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
265 if templ_7745c5c3_Err != nil {265 if templ_7745c5c3_Err != nil {
@@ -282,7 +282,7 @@ func OverlayCmd(items []CmdItem) templ.Component {
282 var templ_7745c5c3_Var11 templ.SafeURL282 var templ_7745c5c3_Var11 templ.SafeURL
283 templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))283 templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
284 if templ_7745c5c3_Err != nil {284 if templ_7745c5c3_Err != nil {
285- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 112, Col: 114}285+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 113, Col: 114}
286 }286 }
287 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))287 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
288 if templ_7745c5c3_Err != nil {288 if templ_7745c5c3_Err != nil {
@@ -295,7 +295,7 @@ func OverlayCmd(items []CmdItem) templ.Component {
295 var templ_7745c5c3_Var12 string295 var templ_7745c5c3_Var12 string
296 templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)296 templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
297 if templ_7745c5c3_Err != nil {297 if templ_7745c5c3_Err != nil {
298- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 112, Col: 158}298+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 113, Col: 158}
299 }299 }
300 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))300 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
301 if templ_7745c5c3_Err != nil {301 if templ_7745c5c3_Err != nil {
@@ -308,7 +308,7 @@ func OverlayCmd(items []CmdItem) templ.Component {
308 var templ_7745c5c3_Var13 string308 var templ_7745c5c3_Var13 string
309 templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(c.Kind)309 templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(c.Kind)
310 if templ_7745c5c3_Err != nil {310 if templ_7745c5c3_Err != nil {
311- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 112, Col: 191}311+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 113, Col: 191}
312 }312 }
313 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))313 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
314 if templ_7745c5c3_Err != nil {314 if templ_7745c5c3_Err != nil {
M internal/adapters/web/static/js/ermite.js +69 -4
@@ -46,10 +46,11 @@
4646 body().dispatchEvent(new CustomEvent('k-' + name, { bubbles: false }));
4747 },
4848
49- // Row cursor: j / k.
49+ // Row cursor: j / k. In the side panel they move the focus between its
50+ // controls instead.
5051 move(d) {
5152 if ($('dialog[open]')) return;
52- if (window.ermiteMR?.stepFiles(d)) return;
53+ if (window.ermiteMR?.stepFiles(d) || window.ermiteRail.step(d)) return;
5354 const rows = visibleRows();
5455 if (!rows.length) {
5556 scrollBy(0, d * 180);
@@ -75,6 +76,16 @@
7576 open() {
7677 if (!$('dialog[open]')) $('[data-cursor] a')?.click();
7778 },
79+ // l / h: into the side panel (the current file of a merge request, else
80+ // its main action, else its first control) and back to the content.
81+ // Enter then activates the control.
82+ rail(into) {
83+ if ($('dialog[open]')) return;
84+ if (into && window.ermiteMR?.focusFiles()) return flash('files · j k move · enter open · h back');
85+ if (!into && window.ermiteMR?.leaveFiles()) return;
86+ const msg = into ? window.ermiteRail.enter() : window.ermiteRail.leave();
87+ if (msg) flash(msg);
88+ },
7889 // openFile opens a file of the diff the table of contents links to.
7990 openFile(id) {
8091 const f = document.getElementById(id);
@@ -215,9 +226,63 @@
215226 };
216227 window.ermite = ermite;
217228
229+ // The side panel (aside.rail) by keyboard: l enters it on its main action
230+ // (the primary button) or its first control, j / k step between its shown
231+ // controls (not hidden, not in a closed <details>), h leaves it for <main>.
232+ // Each returns the hint to flash, if any. Self-contained: jstest/rail.test.mjs
233+ // evaluates this block alone.
234+ window.ermiteRail = (() => {
235+ const CONTROLS = ['a', 'button', 'select', 'summary', 'input', 'textarea'];
236+ const reachable = (e) => {
237+ if (e.disabled || e.closest('[hidden]')) return false;
238+ if (e.tagName === 'A' && !e.hasAttribute('href')) return false;
239+ if (e.tagName === 'INPUT' && e.getAttribute('type') === 'hidden') return false;
240+ return !e.getClientRects || e.getClientRects().length > 0;
241+ };
242+ const rail = () => document.querySelector('.rail');
243+ // items walks the panel in document order.
244+ const items = (r) => {
245+ const out = [];
246+ const walk = (e) => {
247+ for (const c of e.children) {
248+ if (CONTROLS.includes(c.tagName.toLowerCase())) { if (reachable(c)) out.push(c); }
249+ else walk(c);
250+ }
251+ };
252+ walk(r);
253+ return out;
254+ };
255+ const inside = (r) => r && r.contains(document.activeElement);
256+ const go = (e) => { e.focus(); e.scrollIntoView({ block: 'nearest' }); };
257+ return {
258+ enter() {
259+ const r = rail();
260+ if (!r) return 'no side panel on this page';
261+ const list = items(r);
262+ const target = list.find((e) => e.matches('.btn.primary')) || list[0];
263+ if (!target) return 'nothing to act on in the side panel';
264+ go(target);
265+ return 'side panel · j k move · enter act · h back';
266+ },
267+ leave() {
268+ if (inside(rail())) document.getElementById('main')?.focus({ preventScroll: true });
269+ },
270+ // step moves d controls within the panel; false when the focus is not in it.
271+ step(d) {
272+ const r = rail();
273+ if (!inside(r)) return false;
274+ const list = items(r);
275+ const i = list.indexOf(document.activeElement);
276+ const next = list[Math.max(0, Math.min(list.length - 1, i + d))];
277+ if (next) go(next);
278+ return true;
279+ },
280+ };
281+ })();
282+
218283 // 1. Keys.
219284 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',
220- s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc',
285+ s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc',
221286 // the merge request page (mr.js): the merge menu, mark read
222287 M: 'merge', R: 'read' };
223288 // "g t" is the key "t": the tabs have one event each.
@@ -225,7 +290,7 @@
225290 let pending = 0;
226291 let pendingTimer = 0;
227292 addEventListener('keydown', (e) => {
228- const typing = e.target.closest('input, textarea, select, [contenteditable]');
293+ const typing = e.target.closest('input:not([type=checkbox], [type=radio], [type=submit], [type=button]), textarea, select, [contenteditable]');
229294 // ctrl k / cmd k opens the palette; in a field only cmd k, so that the
230295 // field keeps ctrl k (kill to end of line on macOS).
231296 if ((e.metaKey || (e.ctrlKey && !typing)) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'k') { e.preventDefault(); pending = 0; return ermite.fire('cmd'); }
@@ -46,10 +46,11 @@
46 body().dispatchEvent(new CustomEvent('k-' + name, { bubbles: false }));46 body().dispatchEvent(new CustomEvent('k-' + name, { bubbles: false }));
47 },47 },
48 48
49- // Row cursor: j / k.49+ // Row cursor: j / k. In the side panel they move the focus between its
50+ // controls instead.
50 move(d) {51 move(d) {
51 if ($('dialog[open]')) return;52 if ($('dialog[open]')) return;
52- if (window.ermiteMR?.stepFiles(d)) return;53+ if (window.ermiteMR?.stepFiles(d) || window.ermiteRail.step(d)) return;
53 const rows = visibleRows();54 const rows = visibleRows();
54 if (!rows.length) {55 if (!rows.length) {
55 scrollBy(0, d * 180);56 scrollBy(0, d * 180);
@@ -75,6 +76,16 @@
75 open() {76 open() {
76 if (!$('dialog[open]')) $('[data-cursor] a')?.click();77 if (!$('dialog[open]')) $('[data-cursor] a')?.click();
77 },78 },
79+ // l / h: into the side panel (the current file of a merge request, else
80+ // its main action, else its first control) and back to the content.
81+ // Enter then activates the control.
82+ rail(into) {
83+ if ($('dialog[open]')) return;
84+ if (into && window.ermiteMR?.focusFiles()) return flash('files · j k move · enter open · h back');
85+ if (!into && window.ermiteMR?.leaveFiles()) return;
86+ const msg = into ? window.ermiteRail.enter() : window.ermiteRail.leave();
87+ if (msg) flash(msg);
88+ },
78 // openFile opens a file of the diff the table of contents links to.89 // openFile opens a file of the diff the table of contents links to.
79 openFile(id) {90 openFile(id) {
80 const f = document.getElementById(id);91 const f = document.getElementById(id);
@@ -215,9 +226,63 @@
215 };226 };
216 window.ermite = ermite;227 window.ermite = ermite;
217 228
229+ // The side panel (aside.rail) by keyboard: l enters it on its main action
230+ // (the primary button) or its first control, j / k step between its shown
231+ // controls (not hidden, not in a closed <details>), h leaves it for <main>.
232+ // Each returns the hint to flash, if any. Self-contained: jstest/rail.test.mjs
233+ // evaluates this block alone.
234+ window.ermiteRail = (() => {
235+ const CONTROLS = ['a', 'button', 'select', 'summary', 'input', 'textarea'];
236+ const reachable = (e) => {
237+ if (e.disabled || e.closest('[hidden]')) return false;
238+ if (e.tagName === 'A' && !e.hasAttribute('href')) return false;
239+ if (e.tagName === 'INPUT' && e.getAttribute('type') === 'hidden') return false;
240+ return !e.getClientRects || e.getClientRects().length > 0;
241+ };
242+ const rail = () => document.querySelector('.rail');
243+ // items walks the panel in document order.
244+ const items = (r) => {
245+ const out = [];
246+ const walk = (e) => {
247+ for (const c of e.children) {
248+ if (CONTROLS.includes(c.tagName.toLowerCase())) { if (reachable(c)) out.push(c); }
249+ else walk(c);
250+ }
251+ };
252+ walk(r);
253+ return out;
254+ };
255+ const inside = (r) => r && r.contains(document.activeElement);
256+ const go = (e) => { e.focus(); e.scrollIntoView({ block: 'nearest' }); };
257+ return {
258+ enter() {
259+ const r = rail();
260+ if (!r) return 'no side panel on this page';
261+ const list = items(r);
262+ const target = list.find((e) => e.matches('.btn.primary')) || list[0];
263+ if (!target) return 'nothing to act on in the side panel';
264+ go(target);
265+ return 'side panel · j k move · enter act · h back';
266+ },
267+ leave() {
268+ if (inside(rail())) document.getElementById('main')?.focus({ preventScroll: true });
269+ },
270+ // step moves d controls within the panel; false when the focus is not in it.
271+ step(d) {
272+ const r = rail();
273+ if (!inside(r)) return false;
274+ const list = items(r);
275+ const i = list.indexOf(document.activeElement);
276+ const next = list[Math.max(0, Math.min(list.length - 1, i + d))];
277+ if (next) go(next);
278+ return true;
279+ },
280+ };
281+ })();
282+
218 // 1. Keys.283 // 1. Keys.
219 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',284 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',
220- s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc',285+ s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc',
221 // the merge request page (mr.js): the merge menu, mark read286 // the merge request page (mr.js): the merge menu, mark read
222 M: 'merge', R: 'read' };287 M: 'merge', R: 'read' };
223 // "g t" is the key "t": the tabs have one event each.288 // "g t" is the key "t": the tabs have one event each.
@@ -225,7 +290,7 @@
225 let pending = 0;290 let pending = 0;
226 let pendingTimer = 0;291 let pendingTimer = 0;
227 addEventListener('keydown', (e) => {292 addEventListener('keydown', (e) => {
228- const typing = e.target.closest('input, textarea, select, [contenteditable]');293+ const typing = e.target.closest('input:not([type=checkbox], [type=radio], [type=submit], [type=button]), textarea, select, [contenteditable]');
229 // ctrl k / cmd k opens the palette; in a field only cmd k, so that the294 // ctrl k / cmd k opens the palette; in a field only cmd k, so that the
230 // field keeps ctrl k (kill to end of line on macOS).295 // field keeps ctrl k (kill to end of line on macOS).
231 if ((e.metaKey || (e.ctrlKey && !typing)) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'k') { e.preventDefault(); pending = 0; return ermite.fire('cmd'); }296 if ((e.metaKey || (e.ctrlKey && !typing)) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'k') { e.preventDefault(); pending = 0; return ermite.fire('cmd'); }
M internal/app/testdata/web/blob-binary.html +9 -0
@@ -178,6 +178,13 @@
178178 </div>
179179 <div>
180180 <dt>
181+<kbd>l</kbd>
182+<kbd>h</kbd>
183+</dt>
184+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
185+</div>
186+<div>
187+<dt>
181188 <kbd>g g</kbd>
182189 <kbd>G</kbd>
183190 </dt>
@@ -359,6 +366,8 @@
359366 k-top from:body -> ermite.edge(false);
360367 k-bottom from:body -> ermite.edge(true);
361368 k-open from:body -> ermite.open();
369+ k-right from:body -> ermite.rail(true);
370+ k-left from:body -> ermite.rail(false);
362371 k-esc from:body -> ermite.closeDialogs();
363372 k-help from:body -> ermite.dialog('panel-help');
364373 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -178,6 +178,13 @@
178 </div>178 </div>
179 <div>179 <div>
180 <dt>180 <dt>
181+<kbd>l</kbd>
182+<kbd>h</kbd>
183+</dt>
184+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
185+</div>
186+<div>
187+<dt>
181 <kbd>g g</kbd>188 <kbd>g g</kbd>
182 <kbd>G</kbd>189 <kbd>G</kbd>
183 </dt>190 </dt>
@@ -359,6 +366,8 @@
359 k-top from:body -> ermite.edge(false);366 k-top from:body -> ermite.edge(false);
360 k-bottom from:body -> ermite.edge(true);367 k-bottom from:body -> ermite.edge(true);
361 k-open from:body -> ermite.open();368 k-open from:body -> ermite.open();
369+ k-right from:body -> ermite.rail(true);
370+ k-left from:body -> ermite.rail(false);
362 k-esc from:body -> ermite.closeDialogs();371 k-esc from:body -> ermite.closeDialogs();
363 k-help from:body -> ermite.dialog('panel-help');372 k-help from:body -> ermite.dialog('panel-help');
364 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');373 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/blob-code.html +9 -0
@@ -248,6 +248,13 @@
248248 </div>
249249 <div>
250250 <dt>
251+<kbd>l</kbd>
252+<kbd>h</kbd>
253+</dt>
254+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
255+</div>
256+<div>
257+<dt>
251258 <kbd>g g</kbd>
252259 <kbd>G</kbd>
253260 </dt>
@@ -429,6 +436,8 @@
429436 k-top from:body -> ermite.edge(false);
430437 k-bottom from:body -> ermite.edge(true);
431438 k-open from:body -> ermite.open();
439+ k-right from:body -> ermite.rail(true);
440+ k-left from:body -> ermite.rail(false);
432441 k-esc from:body -> ermite.closeDialogs();
433442 k-help from:body -> ermite.dialog('panel-help');
434443 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -248,6 +248,13 @@
248 </div>248 </div>
249 <div>249 <div>
250 <dt>250 <dt>
251+<kbd>l</kbd>
252+<kbd>h</kbd>
253+</dt>
254+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
255+</div>
256+<div>
257+<dt>
251 <kbd>g g</kbd>258 <kbd>g g</kbd>
252 <kbd>G</kbd>259 <kbd>G</kbd>
253 </dt>260 </dt>
@@ -429,6 +436,8 @@
429 k-top from:body -> ermite.edge(false);436 k-top from:body -> ermite.edge(false);
430 k-bottom from:body -> ermite.edge(true);437 k-bottom from:body -> ermite.edge(true);
431 k-open from:body -> ermite.open();438 k-open from:body -> ermite.open();
439+ k-right from:body -> ermite.rail(true);
440+ k-left from:body -> ermite.rail(false);
432 k-esc from:body -> ermite.closeDialogs();441 k-esc from:body -> ermite.closeDialogs();
433 k-help from:body -> ermite.dialog('panel-help');442 k-help from:body -> ermite.dialog('panel-help');
434 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');443 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/blob-empty.html +9 -0
@@ -178,6 +178,13 @@
178178 </div>
179179 <div>
180180 <dt>
181+<kbd>l</kbd>
182+<kbd>h</kbd>
183+</dt>
184+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
185+</div>
186+<div>
187+<dt>
181188 <kbd>g g</kbd>
182189 <kbd>G</kbd>
183190 </dt>
@@ -359,6 +366,8 @@
359366 k-top from:body -> ermite.edge(false);
360367 k-bottom from:body -> ermite.edge(true);
361368 k-open from:body -> ermite.open();
369+ k-right from:body -> ermite.rail(true);
370+ k-left from:body -> ermite.rail(false);
362371 k-esc from:body -> ermite.closeDialogs();
363372 k-help from:body -> ermite.dialog('panel-help');
364373 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -178,6 +178,13 @@
178 </div>178 </div>
179 <div>179 <div>
180 <dt>180 <dt>
181+<kbd>l</kbd>
182+<kbd>h</kbd>
183+</dt>
184+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
185+</div>
186+<div>
187+<dt>
181 <kbd>g g</kbd>188 <kbd>g g</kbd>
182 <kbd>G</kbd>189 <kbd>G</kbd>
183 </dt>190 </dt>
@@ -359,6 +366,8 @@
359 k-top from:body -> ermite.edge(false);366 k-top from:body -> ermite.edge(false);
360 k-bottom from:body -> ermite.edge(true);367 k-bottom from:body -> ermite.edge(true);
361 k-open from:body -> ermite.open();368 k-open from:body -> ermite.open();
369+ k-right from:body -> ermite.rail(true);
370+ k-left from:body -> ermite.rail(false);
362 k-esc from:body -> ermite.closeDialogs();371 k-esc from:body -> ermite.closeDialogs();
363 k-help from:body -> ermite.dialog('panel-help');372 k-help from:body -> ermite.dialog('panel-help');
364 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');373 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/blob-image.html +9 -0
@@ -180,6 +180,13 @@
180180 </div>
181181 <div>
182182 <dt>
183+<kbd>l</kbd>
184+<kbd>h</kbd>
185+</dt>
186+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
187+</div>
188+<div>
189+<dt>
183190 <kbd>g g</kbd>
184191 <kbd>G</kbd>
185192 </dt>
@@ -361,6 +368,8 @@
361368 k-top from:body -> ermite.edge(false);
362369 k-bottom from:body -> ermite.edge(true);
363370 k-open from:body -> ermite.open();
371+ k-right from:body -> ermite.rail(true);
372+ k-left from:body -> ermite.rail(false);
364373 k-esc from:body -> ermite.closeDialogs();
365374 k-help from:body -> ermite.dialog('panel-help');
366375 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -180,6 +180,13 @@
180 </div>180 </div>
181 <div>181 <div>
182 <dt>182 <dt>
183+<kbd>l</kbd>
184+<kbd>h</kbd>
185+</dt>
186+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
187+</div>
188+<div>
189+<dt>
183 <kbd>g g</kbd>190 <kbd>g g</kbd>
184 <kbd>G</kbd>191 <kbd>G</kbd>
185 </dt>192 </dt>
@@ -361,6 +368,8 @@
361 k-top from:body -> ermite.edge(false);368 k-top from:body -> ermite.edge(false);
362 k-bottom from:body -> ermite.edge(true);369 k-bottom from:body -> ermite.edge(true);
363 k-open from:body -> ermite.open();370 k-open from:body -> ermite.open();
371+ k-right from:body -> ermite.rail(true);
372+ k-left from:body -> ermite.rail(false);
364 k-esc from:body -> ermite.closeDialogs();373 k-esc from:body -> ermite.closeDialogs();
365 k-help from:body -> ermite.dialog('panel-help');374 k-help from:body -> ermite.dialog('panel-help');
366 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');375 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/blob-markdown.html +9 -0
@@ -245,6 +245,13 @@
245245 </div>
246246 <div>
247247 <dt>
248+<kbd>l</kbd>
249+<kbd>h</kbd>
250+</dt>
251+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
252+</div>
253+<div>
254+<dt>
248255 <kbd>g g</kbd>
249256 <kbd>G</kbd>
250257 </dt>
@@ -426,6 +433,8 @@
426433 k-top from:body -> ermite.edge(false);
427434 k-bottom from:body -> ermite.edge(true);
428435 k-open from:body -> ermite.open();
436+ k-right from:body -> ermite.rail(true);
437+ k-left from:body -> ermite.rail(false);
429438 k-esc from:body -> ermite.closeDialogs();
430439 k-help from:body -> ermite.dialog('panel-help');
431440 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -245,6 +245,13 @@
245 </div>245 </div>
246 <div>246 <div>
247 <dt>247 <dt>
248+<kbd>l</kbd>
249+<kbd>h</kbd>
250+</dt>
251+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
252+</div>
253+<div>
254+<dt>
248 <kbd>g g</kbd>255 <kbd>g g</kbd>
249 <kbd>G</kbd>256 <kbd>G</kbd>
250 </dt>257 </dt>
@@ -426,6 +433,8 @@
426 k-top from:body -> ermite.edge(false);433 k-top from:body -> ermite.edge(false);
427 k-bottom from:body -> ermite.edge(true);434 k-bottom from:body -> ermite.edge(true);
428 k-open from:body -> ermite.open();435 k-open from:body -> ermite.open();
436+ k-right from:body -> ermite.rail(true);
437+ k-left from:body -> ermite.rail(false);
429 k-esc from:body -> ermite.closeDialogs();438 k-esc from:body -> ermite.closeDialogs();
430 k-help from:body -> ermite.dialog('panel-help');439 k-help from:body -> ermite.dialog('panel-help');
431 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');440 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/blob-svg.html +9 -0
@@ -203,6 +203,13 @@
203203 </div>
204204 <div>
205205 <dt>
206+<kbd>l</kbd>
207+<kbd>h</kbd>
208+</dt>
209+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
210+</div>
211+<div>
212+<dt>
206213 <kbd>g g</kbd>
207214 <kbd>G</kbd>
208215 </dt>
@@ -384,6 +391,8 @@
384391 k-top from:body -> ermite.edge(false);
385392 k-bottom from:body -> ermite.edge(true);
386393 k-open from:body -> ermite.open();
394+ k-right from:body -> ermite.rail(true);
395+ k-left from:body -> ermite.rail(false);
387396 k-esc from:body -> ermite.closeDialogs();
388397 k-help from:body -> ermite.dialog('panel-help');
389398 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -203,6 +203,13 @@
203 </div>203 </div>
204 <div>204 <div>
205 <dt>205 <dt>
206+<kbd>l</kbd>
207+<kbd>h</kbd>
208+</dt>
209+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
210+</div>
211+<div>
212+<dt>
206 <kbd>g g</kbd>213 <kbd>g g</kbd>
207 <kbd>G</kbd>214 <kbd>G</kbd>
208 </dt>215 </dt>
@@ -384,6 +391,8 @@
384 k-top from:body -> ermite.edge(false);391 k-top from:body -> ermite.edge(false);
385 k-bottom from:body -> ermite.edge(true);392 k-bottom from:body -> ermite.edge(true);
386 k-open from:body -> ermite.open();393 k-open from:body -> ermite.open();
394+ k-right from:body -> ermite.rail(true);
395+ k-left from:body -> ermite.rail(false);
387 k-esc from:body -> ermite.closeDialogs();396 k-esc from:body -> ermite.closeDialogs();
388 k-help from:body -> ermite.dialog('panel-help');397 k-help from:body -> ermite.dialog('panel-help');
389 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');398 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/blob-text.html +9 -0
@@ -198,6 +198,13 @@
198198 </div>
199199 <div>
200200 <dt>
201+<kbd>l</kbd>
202+<kbd>h</kbd>
203+</dt>
204+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
205+</div>
206+<div>
207+<dt>
201208 <kbd>g g</kbd>
202209 <kbd>G</kbd>
203210 </dt>
@@ -379,6 +386,8 @@
379386 k-top from:body -> ermite.edge(false);
380387 k-bottom from:body -> ermite.edge(true);
381388 k-open from:body -> ermite.open();
389+ k-right from:body -> ermite.rail(true);
390+ k-left from:body -> ermite.rail(false);
382391 k-esc from:body -> ermite.closeDialogs();
383392 k-help from:body -> ermite.dialog('panel-help');
384393 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -198,6 +198,13 @@
198 </div>198 </div>
199 <div>199 <div>
200 <dt>200 <dt>
201+<kbd>l</kbd>
202+<kbd>h</kbd>
203+</dt>
204+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
205+</div>
206+<div>
207+<dt>
201 <kbd>g g</kbd>208 <kbd>g g</kbd>
202 <kbd>G</kbd>209 <kbd>G</kbd>
203 </dt>210 </dt>
@@ -379,6 +386,8 @@
379 k-top from:body -> ermite.edge(false);386 k-top from:body -> ermite.edge(false);
380 k-bottom from:body -> ermite.edge(true);387 k-bottom from:body -> ermite.edge(true);
381 k-open from:body -> ermite.open();388 k-open from:body -> ermite.open();
389+ k-right from:body -> ermite.rail(true);
390+ k-left from:body -> ermite.rail(false);
382 k-esc from:body -> ermite.closeDialogs();391 k-esc from:body -> ermite.closeDialogs();
383 k-help from:body -> ermite.dialog('panel-help');392 k-help from:body -> ermite.dialog('panel-help');
384 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');393 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/blob-too-large.html +9 -0
@@ -178,6 +178,13 @@
178178 </div>
179179 <div>
180180 <dt>
181+<kbd>l</kbd>
182+<kbd>h</kbd>
183+</dt>
184+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
185+</div>
186+<div>
187+<dt>
181188 <kbd>g g</kbd>
182189 <kbd>G</kbd>
183190 </dt>
@@ -359,6 +366,8 @@
359366 k-top from:body -> ermite.edge(false);
360367 k-bottom from:body -> ermite.edge(true);
361368 k-open from:body -> ermite.open();
369+ k-right from:body -> ermite.rail(true);
370+ k-left from:body -> ermite.rail(false);
362371 k-esc from:body -> ermite.closeDialogs();
363372 k-help from:body -> ermite.dialog('panel-help');
364373 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -178,6 +178,13 @@
178 </div>178 </div>
179 <div>179 <div>
180 <dt>180 <dt>
181+<kbd>l</kbd>
182+<kbd>h</kbd>
183+</dt>
184+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
185+</div>
186+<div>
187+<dt>
181 <kbd>g g</kbd>188 <kbd>g g</kbd>
182 <kbd>G</kbd>189 <kbd>G</kbd>
183 </dt>190 </dt>
@@ -359,6 +366,8 @@
359 k-top from:body -> ermite.edge(false);366 k-top from:body -> ermite.edge(false);
360 k-bottom from:body -> ermite.edge(true);367 k-bottom from:body -> ermite.edge(true);
361 k-open from:body -> ermite.open();368 k-open from:body -> ermite.open();
369+ k-right from:body -> ermite.rail(true);
370+ k-left from:body -> ermite.rail(false);
362 k-esc from:body -> ermite.closeDialogs();371 k-esc from:body -> ermite.closeDialogs();
363 k-help from:body -> ermite.dialog('panel-help');372 k-help from:body -> ermite.dialog('panel-help');
364 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');373 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/blob-trojan.html +9 -0
@@ -245,6 +245,13 @@
245245 </div>
246246 <div>
247247 <dt>
248+<kbd>l</kbd>
249+<kbd>h</kbd>
250+</dt>
251+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
252+</div>
253+<div>
254+<dt>
248255 <kbd>g g</kbd>
249256 <kbd>G</kbd>
250257 </dt>
@@ -426,6 +433,8 @@
426433 k-top from:body -> ermite.edge(false);
427434 k-bottom from:body -> ermite.edge(true);
428435 k-open from:body -> ermite.open();
436+ k-right from:body -> ermite.rail(true);
437+ k-left from:body -> ermite.rail(false);
429438 k-esc from:body -> ermite.closeDialogs();
430439 k-help from:body -> ermite.dialog('panel-help');
431440 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -245,6 +245,13 @@
245 </div>245 </div>
246 <div>246 <div>
247 <dt>247 <dt>
248+<kbd>l</kbd>
249+<kbd>h</kbd>
250+</dt>
251+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
252+</div>
253+<div>
254+<dt>
248 <kbd>g g</kbd>255 <kbd>g g</kbd>
249 <kbd>G</kbd>256 <kbd>G</kbd>
250 </dt>257 </dt>
@@ -426,6 +433,8 @@
426 k-top from:body -> ermite.edge(false);433 k-top from:body -> ermite.edge(false);
427 k-bottom from:body -> ermite.edge(true);434 k-bottom from:body -> ermite.edge(true);
428 k-open from:body -> ermite.open();435 k-open from:body -> ermite.open();
436+ k-right from:body -> ermite.rail(true);
437+ k-left from:body -> ermite.rail(false);
429 k-esc from:body -> ermite.closeDialogs();438 k-esc from:body -> ermite.closeDialogs();
430 k-help from:body -> ermite.dialog('panel-help');439 k-help from:body -> ermite.dialog('panel-help');
431 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');440 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commit-big.html +9 -0
@@ -216,6 +216,13 @@
216216 </div>
217217 <div>
218218 <dt>
219+<kbd>l</kbd>
220+<kbd>h</kbd>
221+</dt>
222+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
223+</div>
224+<div>
225+<dt>
219226 <kbd>g g</kbd>
220227 <kbd>G</kbd>
221228 </dt>
@@ -397,6 +404,8 @@
397404 k-top from:body -> ermite.edge(false);
398405 k-bottom from:body -> ermite.edge(true);
399406 k-open from:body -> ermite.open();
407+ k-right from:body -> ermite.rail(true);
408+ k-left from:body -> ermite.rail(false);
400409 k-esc from:body -> ermite.closeDialogs();
401410 k-help from:body -> ermite.dialog('panel-help');
402411 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -216,6 +216,13 @@
216 </div>216 </div>
217 <div>217 <div>
218 <dt>218 <dt>
219+<kbd>l</kbd>
220+<kbd>h</kbd>
221+</dt>
222+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
223+</div>
224+<div>
225+<dt>
219 <kbd>g g</kbd>226 <kbd>g g</kbd>
220 <kbd>G</kbd>227 <kbd>G</kbd>
221 </dt>228 </dt>
@@ -397,6 +404,8 @@
397 k-top from:body -> ermite.edge(false);404 k-top from:body -> ermite.edge(false);
398 k-bottom from:body -> ermite.edge(true);405 k-bottom from:body -> ermite.edge(true);
399 k-open from:body -> ermite.open();406 k-open from:body -> ermite.open();
407+ k-right from:body -> ermite.rail(true);
408+ k-left from:body -> ermite.rail(false);
400 k-esc from:body -> ermite.closeDialogs();409 k-esc from:body -> ermite.closeDialogs();
401 k-help from:body -> ermite.dialog('panel-help');410 k-help from:body -> ermite.dialog('panel-help');
402 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');411 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commit-binary.html +9 -0
@@ -208,6 +208,13 @@
208208 </div>
209209 <div>
210210 <dt>
211+<kbd>l</kbd>
212+<kbd>h</kbd>
213+</dt>
214+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
215+</div>
216+<div>
217+<dt>
211218 <kbd>g g</kbd>
212219 <kbd>G</kbd>
213220 </dt>
@@ -389,6 +396,8 @@
389396 k-top from:body -> ermite.edge(false);
390397 k-bottom from:body -> ermite.edge(true);
391398 k-open from:body -> ermite.open();
399+ k-right from:body -> ermite.rail(true);
400+ k-left from:body -> ermite.rail(false);
392401 k-esc from:body -> ermite.closeDialogs();
393402 k-help from:body -> ermite.dialog('panel-help');
394403 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -208,6 +208,13 @@
208 </div>208 </div>
209 <div>209 <div>
210 <dt>210 <dt>
211+<kbd>l</kbd>
212+<kbd>h</kbd>
213+</dt>
214+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
215+</div>
216+<div>
217+<dt>
211 <kbd>g g</kbd>218 <kbd>g g</kbd>
212 <kbd>G</kbd>219 <kbd>G</kbd>
213 </dt>220 </dt>
@@ -389,6 +396,8 @@
389 k-top from:body -> ermite.edge(false);396 k-top from:body -> ermite.edge(false);
390 k-bottom from:body -> ermite.edge(true);397 k-bottom from:body -> ermite.edge(true);
391 k-open from:body -> ermite.open();398 k-open from:body -> ermite.open();
399+ k-right from:body -> ermite.rail(true);
400+ k-left from:body -> ermite.rail(false);
392 k-esc from:body -> ermite.closeDialogs();401 k-esc from:body -> ermite.closeDialogs();
393 k-help from:body -> ermite.dialog('panel-help');402 k-help from:body -> ermite.dialog('panel-help');
394 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');403 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commit-merge.html +9 -0
@@ -250,6 +250,13 @@
250250 </div>
251251 <div>
252252 <dt>
253+<kbd>l</kbd>
254+<kbd>h</kbd>
255+</dt>
256+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
257+</div>
258+<div>
259+<dt>
253260 <kbd>g g</kbd>
254261 <kbd>G</kbd>
255262 </dt>
@@ -431,6 +438,8 @@
431438 k-top from:body -> ermite.edge(false);
432439 k-bottom from:body -> ermite.edge(true);
433440 k-open from:body -> ermite.open();
441+ k-right from:body -> ermite.rail(true);
442+ k-left from:body -> ermite.rail(false);
434443 k-esc from:body -> ermite.closeDialogs();
435444 k-help from:body -> ermite.dialog('panel-help');
436445 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -250,6 +250,13 @@
250 </div>250 </div>
251 <div>251 <div>
252 <dt>252 <dt>
253+<kbd>l</kbd>
254+<kbd>h</kbd>
255+</dt>
256+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
257+</div>
258+<div>
259+<dt>
253 <kbd>g g</kbd>260 <kbd>g g</kbd>
254 <kbd>G</kbd>261 <kbd>G</kbd>
255 </dt>262 </dt>
@@ -431,6 +438,8 @@
431 k-top from:body -> ermite.edge(false);438 k-top from:body -> ermite.edge(false);
432 k-bottom from:body -> ermite.edge(true);439 k-bottom from:body -> ermite.edge(true);
433 k-open from:body -> ermite.open();440 k-open from:body -> ermite.open();
441+ k-right from:body -> ermite.rail(true);
442+ k-left from:body -> ermite.rail(false);
434 k-esc from:body -> ermite.closeDialogs();443 k-esc from:body -> ermite.closeDialogs();
435 k-help from:body -> ermite.dialog('panel-help');444 k-help from:body -> ermite.dialog('panel-help');
436 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');445 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commit-modify-split.html +9 -0
@@ -373,6 +373,13 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
373373 </div>
374374 <div>
375375 <dt>
376+<kbd>l</kbd>
377+<kbd>h</kbd>
378+</dt>
379+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
380+</div>
381+<div>
382+<dt>
376383 <kbd>g g</kbd>
377384 <kbd>G</kbd>
378385 </dt>
@@ -554,6 +561,8 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
554561 k-top from:body -> ermite.edge(false);
555562 k-bottom from:body -> ermite.edge(true);
556563 k-open from:body -> ermite.open();
564+ k-right from:body -> ermite.rail(true);
565+ k-left from:body -> ermite.rail(false);
557566 k-esc from:body -> ermite.closeDialogs();
558567 k-help from:body -> ermite.dialog('panel-help');
559568 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -373,6 +373,13 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
373 </div>373 </div>
374 <div>374 <div>
375 <dt>375 <dt>
376+<kbd>l</kbd>
377+<kbd>h</kbd>
378+</dt>
379+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
380+</div>
381+<div>
382+<dt>
376 <kbd>g g</kbd>383 <kbd>g g</kbd>
377 <kbd>G</kbd>384 <kbd>G</kbd>
378 </dt>385 </dt>
@@ -554,6 +561,8 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
554 k-top from:body -> ermite.edge(false);561 k-top from:body -> ermite.edge(false);
555 k-bottom from:body -> ermite.edge(true);562 k-bottom from:body -> ermite.edge(true);
556 k-open from:body -> ermite.open();563 k-open from:body -> ermite.open();
564+ k-right from:body -> ermite.rail(true);
565+ k-left from:body -> ermite.rail(false);
557 k-esc from:body -> ermite.closeDialogs();566 k-esc from:body -> ermite.closeDialogs();
558 k-help from:body -> ermite.dialog('panel-help');567 k-help from:body -> ermite.dialog('panel-help');
559 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');568 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commit-modify.html +9 -0
@@ -373,6 +373,13 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
373373 </div>
374374 <div>
375375 <dt>
376+<kbd>l</kbd>
377+<kbd>h</kbd>
378+</dt>
379+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
380+</div>
381+<div>
382+<dt>
376383 <kbd>g g</kbd>
377384 <kbd>G</kbd>
378385 </dt>
@@ -554,6 +561,8 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
554561 k-top from:body -> ermite.edge(false);
555562 k-bottom from:body -> ermite.edge(true);
556563 k-open from:body -> ermite.open();
564+ k-right from:body -> ermite.rail(true);
565+ k-left from:body -> ermite.rail(false);
557566 k-esc from:body -> ermite.closeDialogs();
558567 k-help from:body -> ermite.dialog('panel-help');
559568 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -373,6 +373,13 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
373 </div>373 </div>
374 <div>374 <div>
375 <dt>375 <dt>
376+<kbd>l</kbd>
377+<kbd>h</kbd>
378+</dt>
379+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
380+</div>
381+<div>
382+<dt>
376 <kbd>g g</kbd>383 <kbd>g g</kbd>
377 <kbd>G</kbd>384 <kbd>G</kbd>
378 </dt>385 </dt>
@@ -554,6 +561,8 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
554 k-top from:body -> ermite.edge(false);561 k-top from:body -> ermite.edge(false);
555 k-bottom from:body -> ermite.edge(true);562 k-bottom from:body -> ermite.edge(true);
556 k-open from:body -> ermite.open();563 k-open from:body -> ermite.open();
564+ k-right from:body -> ermite.rail(true);
565+ k-left from:body -> ermite.rail(false);
557 k-esc from:body -> ermite.closeDialogs();566 k-esc from:body -> ermite.closeDialogs();
558 k-help from:body -> ermite.dialog('panel-help');567 k-help from:body -> ermite.dialog('panel-help');
559 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');568 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commit-rename.html +9 -0
@@ -277,6 +277,13 @@
277277 </div>
278278 <div>
279279 <dt>
280+<kbd>l</kbd>
281+<kbd>h</kbd>
282+</dt>
283+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
284+</div>
285+<div>
286+<dt>
280287 <kbd>g g</kbd>
281288 <kbd>G</kbd>
282289 </dt>
@@ -458,6 +465,8 @@
458465 k-top from:body -> ermite.edge(false);
459466 k-bottom from:body -> ermite.edge(true);
460467 k-open from:body -> ermite.open();
468+ k-right from:body -> ermite.rail(true);
469+ k-left from:body -> ermite.rail(false);
461470 k-esc from:body -> ermite.closeDialogs();
462471 k-help from:body -> ermite.dialog('panel-help');
463472 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -277,6 +277,13 @@
277 </div>277 </div>
278 <div>278 <div>
279 <dt>279 <dt>
280+<kbd>l</kbd>
281+<kbd>h</kbd>
282+</dt>
283+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
284+</div>
285+<div>
286+<dt>
280 <kbd>g g</kbd>287 <kbd>g g</kbd>
281 <kbd>G</kbd>288 <kbd>G</kbd>
282 </dt>289 </dt>
@@ -458,6 +465,8 @@
458 k-top from:body -> ermite.edge(false);465 k-top from:body -> ermite.edge(false);
459 k-bottom from:body -> ermite.edge(true);466 k-bottom from:body -> ermite.edge(true);
460 k-open from:body -> ermite.open();467 k-open from:body -> ermite.open();
468+ k-right from:body -> ermite.rail(true);
469+ k-left from:body -> ermite.rail(false);
461 k-esc from:body -> ermite.closeDialogs();470 k-esc from:body -> ermite.closeDialogs();
462 k-help from:body -> ermite.dialog('panel-help');471 k-help from:body -> ermite.dialog('panel-help');
463 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');472 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commit-root.html +9 -0
@@ -596,6 +596,13 @@
596596 </div>
597597 <div>
598598 <dt>
599+<kbd>l</kbd>
600+<kbd>h</kbd>
601+</dt>
602+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
603+</div>
604+<div>
605+<dt>
599606 <kbd>g g</kbd>
600607 <kbd>G</kbd>
601608 </dt>
@@ -777,6 +784,8 @@
777784 k-top from:body -> ermite.edge(false);
778785 k-bottom from:body -> ermite.edge(true);
779786 k-open from:body -> ermite.open();
787+ k-right from:body -> ermite.rail(true);
788+ k-left from:body -> ermite.rail(false);
780789 k-esc from:body -> ermite.closeDialogs();
781790 k-help from:body -> ermite.dialog('panel-help');
782791 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -596,6 +596,13 @@
596 </div>596 </div>
597 <div>597 <div>
598 <dt>598 <dt>
599+<kbd>l</kbd>
600+<kbd>h</kbd>
601+</dt>
602+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
603+</div>
604+<div>
605+<dt>
599 <kbd>g g</kbd>606 <kbd>g g</kbd>
600 <kbd>G</kbd>607 <kbd>G</kbd>
601 </dt>608 </dt>
@@ -777,6 +784,8 @@
777 k-top from:body -> ermite.edge(false);784 k-top from:body -> ermite.edge(false);
778 k-bottom from:body -> ermite.edge(true);785 k-bottom from:body -> ermite.edge(true);
779 k-open from:body -> ermite.open();786 k-open from:body -> ermite.open();
787+ k-right from:body -> ermite.rail(true);
788+ k-left from:body -> ermite.rail(false);
780 k-esc from:body -> ermite.closeDialogs();789 k-esc from:body -> ermite.closeDialogs();
781 k-help from:body -> ermite.dialog('panel-help');790 k-help from:body -> ermite.dialog('panel-help');
782 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');791 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commit-trojan.html +9 -0
@@ -385,6 +385,13 @@
385385 </div>
386386 <div>
387387 <dt>
388+<kbd>l</kbd>
389+<kbd>h</kbd>
390+</dt>
391+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
392+</div>
393+<div>
394+<dt>
388395 <kbd>g g</kbd>
389396 <kbd>G</kbd>
390397 </dt>
@@ -566,6 +573,8 @@
566573 k-top from:body -> ermite.edge(false);
567574 k-bottom from:body -> ermite.edge(true);
568575 k-open from:body -> ermite.open();
576+ k-right from:body -> ermite.rail(true);
577+ k-left from:body -> ermite.rail(false);
569578 k-esc from:body -> ermite.closeDialogs();
570579 k-help from:body -> ermite.dialog('panel-help');
571580 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -385,6 +385,13 @@
385 </div>385 </div>
386 <div>386 <div>
387 <dt>387 <dt>
388+<kbd>l</kbd>
389+<kbd>h</kbd>
390+</dt>
391+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
392+</div>
393+<div>
394+<dt>
388 <kbd>g g</kbd>395 <kbd>g g</kbd>
389 <kbd>G</kbd>396 <kbd>G</kbd>
390 </dt>397 </dt>
@@ -566,6 +573,8 @@
566 k-top from:body -> ermite.edge(false);573 k-top from:body -> ermite.edge(false);
567 k-bottom from:body -> ermite.edge(true);574 k-bottom from:body -> ermite.edge(true);
568 k-open from:body -> ermite.open();575 k-open from:body -> ermite.open();
576+ k-right from:body -> ermite.rail(true);
577+ k-left from:body -> ermite.rail(false);
569 k-esc from:body -> ermite.closeDialogs();578 k-esc from:body -> ermite.closeDialogs();
570 k-help from:body -> ermite.dialog('panel-help');579 k-help from:body -> ermite.dialog('panel-help');
571 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');580 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commits-page2.html +9 -0
@@ -209,6 +209,13 @@
209209 </div>
210210 <div>
211211 <dt>
212+<kbd>l</kbd>
213+<kbd>h</kbd>
214+</dt>
215+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
216+</div>
217+<div>
218+<dt>
212219 <kbd>g g</kbd>
213220 <kbd>G</kbd>
214221 </dt>
@@ -376,6 +383,8 @@
376383 k-top from:body -> ermite.edge(false);
377384 k-bottom from:body -> ermite.edge(true);
378385 k-open from:body -> ermite.open();
386+ k-right from:body -> ermite.rail(true);
387+ k-left from:body -> ermite.rail(false);
379388 k-esc from:body -> ermite.closeDialogs();
380389 k-help from:body -> ermite.dialog('panel-help');
381390 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -209,6 +209,13 @@
209 </div>209 </div>
210 <div>210 <div>
211 <dt>211 <dt>
212+<kbd>l</kbd>
213+<kbd>h</kbd>
214+</dt>
215+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
216+</div>
217+<div>
218+<dt>
212 <kbd>g g</kbd>219 <kbd>g g</kbd>
213 <kbd>G</kbd>220 <kbd>G</kbd>
214 </dt>221 </dt>
@@ -376,6 +383,8 @@
376 k-top from:body -> ermite.edge(false);383 k-top from:body -> ermite.edge(false);
377 k-bottom from:body -> ermite.edge(true);384 k-bottom from:body -> ermite.edge(true);
378 k-open from:body -> ermite.open();385 k-open from:body -> ermite.open();
386+ k-right from:body -> ermite.rail(true);
387+ k-left from:body -> ermite.rail(false);
379 k-esc from:body -> ermite.closeDialogs();388 k-esc from:body -> ermite.closeDialogs();
380 k-help from:body -> ermite.dialog('panel-help');389 k-help from:body -> ermite.dialog('panel-help');
381 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');390 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commits-path.html +9 -0
@@ -224,6 +224,13 @@
224224 </div>
225225 <div>
226226 <dt>
227+<kbd>l</kbd>
228+<kbd>h</kbd>
229+</dt>
230+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
231+</div>
232+<div>
233+<dt>
227234 <kbd>g g</kbd>
228235 <kbd>G</kbd>
229236 </dt>
@@ -391,6 +398,8 @@
391398 k-top from:body -> ermite.edge(false);
392399 k-bottom from:body -> ermite.edge(true);
393400 k-open from:body -> ermite.open();
401+ k-right from:body -> ermite.rail(true);
402+ k-left from:body -> ermite.rail(false);
394403 k-esc from:body -> ermite.closeDialogs();
395404 k-help from:body -> ermite.dialog('panel-help');
396405 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -224,6 +224,13 @@
224 </div>224 </div>
225 <div>225 <div>
226 <dt>226 <dt>
227+<kbd>l</kbd>
228+<kbd>h</kbd>
229+</dt>
230+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
231+</div>
232+<div>
233+<dt>
227 <kbd>g g</kbd>234 <kbd>g g</kbd>
228 <kbd>G</kbd>235 <kbd>G</kbd>
229 </dt>236 </dt>
@@ -391,6 +398,8 @@
391 k-top from:body -> ermite.edge(false);398 k-top from:body -> ermite.edge(false);
392 k-bottom from:body -> ermite.edge(true);399 k-bottom from:body -> ermite.edge(true);
393 k-open from:body -> ermite.open();400 k-open from:body -> ermite.open();
401+ k-right from:body -> ermite.rail(true);
402+ k-left from:body -> ermite.rail(false);
394 k-esc from:body -> ermite.closeDialogs();403 k-esc from:body -> ermite.closeDialogs();
395 k-help from:body -> ermite.dialog('panel-help');404 k-help from:body -> ermite.dialog('panel-help');
396 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');405 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commits-root.html +9 -0
@@ -1109,6 +1109,13 @@
11091109 </div>
11101110 <div>
11111111 <dt>
1112+<kbd>l</kbd>
1113+<kbd>h</kbd>
1114+</dt>
1115+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
1116+</div>
1117+<div>
1118+<dt>
11121119 <kbd>g g</kbd>
11131120 <kbd>G</kbd>
11141121 </dt>
@@ -1276,6 +1283,8 @@
12761283 k-top from:body -> ermite.edge(false);
12771284 k-bottom from:body -> ermite.edge(true);
12781285 k-open from:body -> ermite.open();
1286+ k-right from:body -> ermite.rail(true);
1287+ k-left from:body -> ermite.rail(false);
12791288 k-esc from:body -> ermite.closeDialogs();
12801289 k-help from:body -> ermite.dialog('panel-help');
12811290 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -1109,6 +1109,13 @@
1109 </div>1109 </div>
1110 <div>1110 <div>
1111 <dt>1111 <dt>
1112+<kbd>l</kbd>
1113+<kbd>h</kbd>
1114+</dt>
1115+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
1116+</div>
1117+<div>
1118+<dt>
1112 <kbd>g g</kbd>1119 <kbd>g g</kbd>
1113 <kbd>G</kbd>1120 <kbd>G</kbd>
1114 </dt>1121 </dt>
@@ -1276,6 +1283,8 @@
1276 k-top from:body -> ermite.edge(false);1283 k-top from:body -> ermite.edge(false);
1277 k-bottom from:body -> ermite.edge(true);1284 k-bottom from:body -> ermite.edge(true);
1278 k-open from:body -> ermite.open();1285 k-open from:body -> ermite.open();
1286+ k-right from:body -> ermite.rail(true);
1287+ k-left from:body -> ermite.rail(false);
1279 k-esc from:body -> ermite.closeDialogs();1288 k-esc from:body -> ermite.closeDialogs();
1280 k-help from:body -> ermite.dialog('panel-help');1289 k-help from:body -> ermite.dialog('panel-help');
1281 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');1290 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/commits-small.html +9 -0
@@ -370,6 +370,13 @@
370370 </div>
371371 <div>
372372 <dt>
373+<kbd>l</kbd>
374+<kbd>h</kbd>
375+</dt>
376+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
377+</div>
378+<div>
379+<dt>
373380 <kbd>g g</kbd>
374381 <kbd>G</kbd>
375382 </dt>
@@ -557,6 +564,8 @@
557564 k-top from:body -> ermite.edge(false);
558565 k-bottom from:body -> ermite.edge(true);
559566 k-open from:body -> ermite.open();
567+ k-right from:body -> ermite.rail(true);
568+ k-left from:body -> ermite.rail(false);
560569 k-esc from:body -> ermite.closeDialogs();
561570 k-help from:body -> ermite.dialog('panel-help');
562571 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -370,6 +370,13 @@
370 </div>370 </div>
371 <div>371 <div>
372 <dt>372 <dt>
373+<kbd>l</kbd>
374+<kbd>h</kbd>
375+</dt>
376+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
377+</div>
378+<div>
379+<dt>
373 <kbd>g g</kbd>380 <kbd>g g</kbd>
374 <kbd>G</kbd>381 <kbd>G</kbd>
375 </dt>382 </dt>
@@ -557,6 +564,8 @@
557 k-top from:body -> ermite.edge(false);564 k-top from:body -> ermite.edge(false);
558 k-bottom from:body -> ermite.edge(true);565 k-bottom from:body -> ermite.edge(true);
559 k-open from:body -> ermite.open();566 k-open from:body -> ermite.open();
567+ k-right from:body -> ermite.rail(true);
568+ k-left from:body -> ermite.rail(false);
560 k-esc from:body -> ermite.closeDialogs();569 k-esc from:body -> ermite.closeDialogs();
561 k-help from:body -> ermite.dialog('panel-help');570 k-help from:body -> ermite.dialog('panel-help');
562 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');571 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/empty.html +9 -0
@@ -127,6 +127,13 @@
127127 </div>
128128 <div>
129129 <dt>
130+<kbd>l</kbd>
131+<kbd>h</kbd>
132+</dt>
133+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
134+</div>
135+<div>
136+<dt>
130137 <kbd>g g</kbd>
131138 <kbd>G</kbd>
132139 </dt>
@@ -225,6 +232,8 @@
225232 k-top from:body -> ermite.edge(false);
226233 k-bottom from:body -> ermite.edge(true);
227234 k-open from:body -> ermite.open();
235+ k-right from:body -> ermite.rail(true);
236+ k-left from:body -> ermite.rail(false);
228237 k-esc from:body -> ermite.closeDialogs();
229238 k-help from:body -> ermite.dialog('panel-help');
230239 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -127,6 +127,13 @@
127 </div>127 </div>
128 <div>128 <div>
129 <dt>129 <dt>
130+<kbd>l</kbd>
131+<kbd>h</kbd>
132+</dt>
133+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
134+</div>
135+<div>
136+<dt>
130 <kbd>g g</kbd>137 <kbd>g g</kbd>
131 <kbd>G</kbd>138 <kbd>G</kbd>
132 </dt>139 </dt>
@@ -225,6 +232,8 @@
225 k-top from:body -> ermite.edge(false);232 k-top from:body -> ermite.edge(false);
226 k-bottom from:body -> ermite.edge(true);233 k-bottom from:body -> ermite.edge(true);
227 k-open from:body -> ermite.open();234 k-open from:body -> ermite.open();
235+ k-right from:body -> ermite.rail(true);
236+ k-left from:body -> ermite.rail(false);
228 k-esc from:body -> ermite.closeDialogs();237 k-esc from:body -> ermite.closeDialogs();
229 k-help from:body -> ermite.dialog('panel-help');238 k-help from:body -> ermite.dialog('panel-help');
230 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');239 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/home.html +9 -0
@@ -159,6 +159,13 @@
159159 </div>
160160 <div>
161161 <dt>
162+<kbd>l</kbd>
163+<kbd>h</kbd>
164+</dt>
165+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
166+</div>
167+<div>
168+<dt>
162169 <kbd>g g</kbd>
163170 <kbd>G</kbd>
164171 </dt>
@@ -263,6 +270,8 @@
263270 k-top from:body -> ermite.edge(false);
264271 k-bottom from:body -> ermite.edge(true);
265272 k-open from:body -> ermite.open();
273+ k-right from:body -> ermite.rail(true);
274+ k-left from:body -> ermite.rail(false);
266275 k-esc from:body -> ermite.closeDialogs();
267276 k-help from:body -> ermite.dialog('panel-help');
268277 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -159,6 +159,13 @@
159 </div>159 </div>
160 <div>160 <div>
161 <dt>161 <dt>
162+<kbd>l</kbd>
163+<kbd>h</kbd>
164+</dt>
165+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
166+</div>
167+<div>
168+<dt>
162 <kbd>g g</kbd>169 <kbd>g g</kbd>
163 <kbd>G</kbd>170 <kbd>G</kbd>
164 </dt>171 </dt>
@@ -263,6 +270,8 @@
263 k-top from:body -> ermite.edge(false);270 k-top from:body -> ermite.edge(false);
264 k-bottom from:body -> ermite.edge(true);271 k-bottom from:body -> ermite.edge(true);
265 k-open from:body -> ermite.open();272 k-open from:body -> ermite.open();
273+ k-right from:body -> ermite.rail(true);
274+ k-left from:body -> ermite.rail(false);
266 k-esc from:body -> ermite.closeDialogs();275 k-esc from:body -> ermite.closeDialogs();
267 k-help from:body -> ermite.dialog('panel-help');276 k-help from:body -> ermite.dialog('panel-help');
268 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');277 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/not-found-page.html +9 -0
@@ -116,6 +116,13 @@
116116 </div>
117117 <div>
118118 <dt>
119+<kbd>l</kbd>
120+<kbd>h</kbd>
121+</dt>
122+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
123+</div>
124+<div>
125+<dt>
119126 <kbd>g g</kbd>
120127 <kbd>G</kbd>
121128 </dt>
@@ -208,6 +215,8 @@
208215 k-top from:body -> ermite.edge(false);
209216 k-bottom from:body -> ermite.edge(true);
210217 k-open from:body -> ermite.open();
218+ k-right from:body -> ermite.rail(true);
219+ k-left from:body -> ermite.rail(false);
211220 k-esc from:body -> ermite.closeDialogs();
212221 k-help from:body -> ermite.dialog('panel-help');
213222 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -116,6 +116,13 @@
116 </div>116 </div>
117 <div>117 <div>
118 <dt>118 <dt>
119+<kbd>l</kbd>
120+<kbd>h</kbd>
121+</dt>
122+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
123+</div>
124+<div>
125+<dt>
119 <kbd>g g</kbd>126 <kbd>g g</kbd>
120 <kbd>G</kbd>127 <kbd>G</kbd>
121 </dt>128 </dt>
@@ -208,6 +215,8 @@
208 k-top from:body -> ermite.edge(false);215 k-top from:body -> ermite.edge(false);
209 k-bottom from:body -> ermite.edge(true);216 k-bottom from:body -> ermite.edge(true);
210 k-open from:body -> ermite.open();217 k-open from:body -> ermite.open();
218+ k-right from:body -> ermite.rail(true);
219+ k-left from:body -> ermite.rail(false);
211 k-esc from:body -> ermite.closeDialogs();220 k-esc from:body -> ermite.closeDialogs();
212 k-help from:body -> ermite.dialog('panel-help');221 k-help from:body -> ermite.dialog('panel-help');
213 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');222 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/not-found.html +9 -0
@@ -116,6 +116,13 @@
116116 </div>
117117 <div>
118118 <dt>
119+<kbd>l</kbd>
120+<kbd>h</kbd>
121+</dt>
122+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
123+</div>
124+<div>
125+<dt>
119126 <kbd>g g</kbd>
120127 <kbd>G</kbd>
121128 </dt>
@@ -208,6 +215,8 @@
208215 k-top from:body -> ermite.edge(false);
209216 k-bottom from:body -> ermite.edge(true);
210217 k-open from:body -> ermite.open();
218+ k-right from:body -> ermite.rail(true);
219+ k-left from:body -> ermite.rail(false);
211220 k-esc from:body -> ermite.closeDialogs();
212221 k-help from:body -> ermite.dialog('panel-help');
213222 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -116,6 +116,13 @@
116 </div>116 </div>
117 <div>117 <div>
118 <dt>118 <dt>
119+<kbd>l</kbd>
120+<kbd>h</kbd>
121+</dt>
122+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
123+</div>
124+<div>
125+<dt>
119 <kbd>g g</kbd>126 <kbd>g g</kbd>
120 <kbd>G</kbd>127 <kbd>G</kbd>
121 </dt>128 </dt>
@@ -208,6 +215,8 @@
208 k-top from:body -> ermite.edge(false);215 k-top from:body -> ermite.edge(false);
209 k-bottom from:body -> ermite.edge(true);216 k-bottom from:body -> ermite.edge(true);
210 k-open from:body -> ermite.open();217 k-open from:body -> ermite.open();
218+ k-right from:body -> ermite.rail(true);
219+ k-left from:body -> ermite.rail(false);
211 k-esc from:body -> ermite.closeDialogs();220 k-esc from:body -> ermite.closeDialogs();
212 k-help from:body -> ermite.dialog('panel-help');221 k-help from:body -> ermite.dialog('panel-help');
213 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');222 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/refs.html +9 -0
@@ -246,6 +246,13 @@
246246 </div>
247247 <div>
248248 <dt>
249+<kbd>l</kbd>
250+<kbd>h</kbd>
251+</dt>
252+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
253+</div>
254+<div>
255+<dt>
249256 <kbd>g g</kbd>
250257 <kbd>G</kbd>
251258 </dt>
@@ -443,6 +450,8 @@
443450 k-top from:body -> ermite.edge(false);
444451 k-bottom from:body -> ermite.edge(true);
445452 k-open from:body -> ermite.open();
453+ k-right from:body -> ermite.rail(true);
454+ k-left from:body -> ermite.rail(false);
446455 k-esc from:body -> ermite.closeDialogs();
447456 k-help from:body -> ermite.dialog('panel-help');
448457 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -246,6 +246,13 @@
246 </div>246 </div>
247 <div>247 <div>
248 <dt>248 <dt>
249+<kbd>l</kbd>
250+<kbd>h</kbd>
251+</dt>
252+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
253+</div>
254+<div>
255+<dt>
249 <kbd>g g</kbd>256 <kbd>g g</kbd>
250 <kbd>G</kbd>257 <kbd>G</kbd>
251 </dt>258 </dt>
@@ -443,6 +450,8 @@
443 k-top from:body -> ermite.edge(false);450 k-top from:body -> ermite.edge(false);
444 k-bottom from:body -> ermite.edge(true);451 k-bottom from:body -> ermite.edge(true);
445 k-open from:body -> ermite.open();452 k-open from:body -> ermite.open();
453+ k-right from:body -> ermite.rail(true);
454+ k-left from:body -> ermite.rail(false);
446 k-esc from:body -> ermite.closeDialogs();455 k-esc from:body -> ermite.closeDialogs();
447 k-help from:body -> ermite.dialog('panel-help');456 k-help from:body -> ermite.dialog('panel-help');
448 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');457 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/summary.html +9 -0
@@ -275,6 +275,13 @@
275275 </div>
276276 <div>
277277 <dt>
278+<kbd>l</kbd>
279+<kbd>h</kbd>
280+</dt>
281+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
282+</div>
283+<div>
284+<dt>
278285 <kbd>g g</kbd>
279286 <kbd>G</kbd>
280287 </dt>
@@ -472,6 +479,8 @@
472479 k-top from:body -> ermite.edge(false);
473480 k-bottom from:body -> ermite.edge(true);
474481 k-open from:body -> ermite.open();
482+ k-right from:body -> ermite.rail(true);
483+ k-left from:body -> ermite.rail(false);
475484 k-esc from:body -> ermite.closeDialogs();
476485 k-help from:body -> ermite.dialog('panel-help');
477486 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -275,6 +275,13 @@
275 </div>275 </div>
276 <div>276 <div>
277 <dt>277 <dt>
278+<kbd>l</kbd>
279+<kbd>h</kbd>
280+</dt>
281+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
282+</div>
283+<div>
284+<dt>
278 <kbd>g g</kbd>285 <kbd>g g</kbd>
279 <kbd>G</kbd>286 <kbd>G</kbd>
280 </dt>287 </dt>
@@ -472,6 +479,8 @@
472 k-top from:body -> ermite.edge(false);479 k-top from:body -> ermite.edge(false);
473 k-bottom from:body -> ermite.edge(true);480 k-bottom from:body -> ermite.edge(true);
474 k-open from:body -> ermite.open();481 k-open from:body -> ermite.open();
482+ k-right from:body -> ermite.rail(true);
483+ k-left from:body -> ermite.rail(false);
475 k-esc from:body -> ermite.closeDialogs();484 k-esc from:body -> ermite.closeDialogs();
476 k-help from:body -> ermite.dialog('panel-help');485 k-help from:body -> ermite.dialog('panel-help');
477 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');486 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/tree-nested.html +9 -0
@@ -198,6 +198,13 @@
198198 </div>
199199 <div>
200200 <dt>
201+<kbd>l</kbd>
202+<kbd>h</kbd>
203+</dt>
204+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
205+</div>
206+<div>
207+<dt>
201208 <kbd>g g</kbd>
202209 <kbd>G</kbd>
203210 </dt>
@@ -379,6 +386,8 @@
379386 k-top from:body -> ermite.edge(false);
380387 k-bottom from:body -> ermite.edge(true);
381388 k-open from:body -> ermite.open();
389+ k-right from:body -> ermite.rail(true);
390+ k-left from:body -> ermite.rail(false);
382391 k-esc from:body -> ermite.closeDialogs();
383392 k-help from:body -> ermite.dialog('panel-help');
384393 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -198,6 +198,13 @@
198 </div>198 </div>
199 <div>199 <div>
200 <dt>200 <dt>
201+<kbd>l</kbd>
202+<kbd>h</kbd>
203+</dt>
204+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
205+</div>
206+<div>
207+<dt>
201 <kbd>g g</kbd>208 <kbd>g g</kbd>
202 <kbd>G</kbd>209 <kbd>G</kbd>
203 </dt>210 </dt>
@@ -379,6 +386,8 @@
379 k-top from:body -> ermite.edge(false);386 k-top from:body -> ermite.edge(false);
380 k-bottom from:body -> ermite.edge(true);387 k-bottom from:body -> ermite.edge(true);
381 k-open from:body -> ermite.open();388 k-open from:body -> ermite.open();
389+ k-right from:body -> ermite.rail(true);
390+ k-left from:body -> ermite.rail(false);
382 k-esc from:body -> ermite.closeDialogs();391 k-esc from:body -> ermite.closeDialogs();
383 k-help from:body -> ermite.dialog('panel-help');392 k-help from:body -> ermite.dialog('panel-help');
384 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');393 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/tree-root.html +9 -0
@@ -425,6 +425,13 @@
425425 </div>
426426 <div>
427427 <dt>
428+<kbd>l</kbd>
429+<kbd>h</kbd>
430+</dt>
431+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
432+</div>
433+<div>
434+<dt>
428435 <kbd>g g</kbd>
429436 <kbd>G</kbd>
430437 </dt>
@@ -606,6 +613,8 @@
606613 k-top from:body -> ermite.edge(false);
607614 k-bottom from:body -> ermite.edge(true);
608615 k-open from:body -> ermite.open();
616+ k-right from:body -> ermite.rail(true);
617+ k-left from:body -> ermite.rail(false);
609618 k-esc from:body -> ermite.closeDialogs();
610619 k-help from:body -> ermite.dialog('panel-help');
611620 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -425,6 +425,13 @@
425 </div>425 </div>
426 <div>426 <div>
427 <dt>427 <dt>
428+<kbd>l</kbd>
429+<kbd>h</kbd>
430+</dt>
431+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
432+</div>
433+<div>
434+<dt>
428 <kbd>g g</kbd>435 <kbd>g g</kbd>
429 <kbd>G</kbd>436 <kbd>G</kbd>
430 </dt>437 </dt>
@@ -606,6 +613,8 @@
606 k-top from:body -> ermite.edge(false);613 k-top from:body -> ermite.edge(false);
607 k-bottom from:body -> ermite.edge(true);614 k-bottom from:body -> ermite.edge(true);
608 k-open from:body -> ermite.open();615 k-open from:body -> ermite.open();
616+ k-right from:body -> ermite.rail(true);
617+ k-left from:body -> ermite.rail(false);
609 k-esc from:body -> ermite.closeDialogs();618 k-esc from:body -> ermite.closeDialogs();
610 k-help from:body -> ermite.dialog('panel-help');619 k-help from:body -> ermite.dialog('panel-help');
611 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');620 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/tree-slash-ref.html +9 -0
@@ -195,6 +195,13 @@
195195 </div>
196196 <div>
197197 <dt>
198+<kbd>l</kbd>
199+<kbd>h</kbd>
200+</dt>
201+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
202+</div>
203+<div>
204+<dt>
198205 <kbd>g g</kbd>
199206 <kbd>G</kbd>
200207 </dt>
@@ -376,6 +383,8 @@
376383 k-top from:body -> ermite.edge(false);
377384 k-bottom from:body -> ermite.edge(true);
378385 k-open from:body -> ermite.open();
386+ k-right from:body -> ermite.rail(true);
387+ k-left from:body -> ermite.rail(false);
379388 k-esc from:body -> ermite.closeDialogs();
380389 k-help from:body -> ermite.dialog('panel-help');
381390 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -195,6 +195,13 @@
195 </div>195 </div>
196 <div>196 <div>
197 <dt>197 <dt>
198+<kbd>l</kbd>
199+<kbd>h</kbd>
200+</dt>
201+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
202+</div>
203+<div>
204+<dt>
198 <kbd>g g</kbd>205 <kbd>g g</kbd>
199 <kbd>G</kbd>206 <kbd>G</kbd>
200 </dt>207 </dt>
@@ -376,6 +383,8 @@
376 k-top from:body -> ermite.edge(false);383 k-top from:body -> ermite.edge(false);
377 k-bottom from:body -> ermite.edge(true);384 k-bottom from:body -> ermite.edge(true);
378 k-open from:body -> ermite.open();385 k-open from:body -> ermite.open();
386+ k-right from:body -> ermite.rail(true);
387+ k-left from:body -> ermite.rail(false);
379 k-esc from:body -> ermite.closeDialogs();388 k-esc from:body -> ermite.closeDialogs();
380 k-help from:body -> ermite.dialog('panel-help');389 k-help from:body -> ermite.dialog('panel-help');
381 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');390 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
M internal/app/testdata/web/tree-subdir.html +9 -0
@@ -220,6 +220,13 @@
220220 </div>
221221 <div>
222222 <dt>
223+<kbd>l</kbd>
224+<kbd>h</kbd>
225+</dt>
226+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
227+</div>
228+<div>
229+<dt>
223230 <kbd>g g</kbd>
224231 <kbd>G</kbd>
225232 </dt>
@@ -401,6 +408,8 @@
401408 k-top from:body -> ermite.edge(false);
402409 k-bottom from:body -> ermite.edge(true);
403410 k-open from:body -> ermite.open();
411+ k-right from:body -> ermite.rail(true);
412+ k-left from:body -> ermite.rail(false);
404413 k-esc from:body -> ermite.closeDialogs();
405414 k-help from:body -> ermite.dialog('panel-help');
406415 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');
@@ -220,6 +220,13 @@
220 </div>220 </div>
221 <div>221 <div>
222 <dt>222 <dt>
223+<kbd>l</kbd>
224+<kbd>h</kbd>
225+</dt>
226+<dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd>
227+</div>
228+<div>
229+<dt>
223 <kbd>g g</kbd>230 <kbd>g g</kbd>
224 <kbd>G</kbd>231 <kbd>G</kbd>
225 </dt>232 </dt>
@@ -401,6 +408,8 @@
401 k-top from:body -> ermite.edge(false);408 k-top from:body -> ermite.edge(false);
402 k-bottom from:body -> ermite.edge(true);409 k-bottom from:body -> ermite.edge(true);
403 k-open from:body -> ermite.open();410 k-open from:body -> ermite.open();
411+ k-right from:body -> ermite.rail(true);
412+ k-left from:body -> ermite.rail(false);
404 k-esc from:body -> ermite.closeDialogs();413 k-esc from:body -> ermite.closeDialogs();
405 k-help from:body -> ermite.dialog('panel-help');414 k-help from:body -> ermite.dialog('panel-help');
406 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');415 k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');

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