feat(web): browse files as a directory tree with keyboard navigation (#58)
- commit
- 2bf61ae1b3afdfca9ad52064044ce67f22d1303d
- author
- brice · · 2026-10-18 06:27 UTC
- committer
- bnema · · 2026-10-18 06:27 UTC
- parent
- e95eebd
- signature
- SSH Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe key is unknown
45 files · +2002 -792
M internal/adapters/web/ASSETS.md +1 -1
| @@ -10,7 +10,7 @@ content-hashed names. Nothing is fetched at runtime. | |||
| 10 | 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | |
| 11 | 11 | | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | | | |
| 12 | 12 | | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | | | |
| 13 | - | | `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | | | |
| 13 | + | | `static/js/files.js` | the Files view: a tree whose directories open in place and walk by keyboard (`window.ermiteTree`, tested in Node by `jstest/tree.test.mjs`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | | | |
| 14 | 14 | | `static/js/passkey.js` | passkey sign-in on `/-/login` and passkey registration on `/-/passkeys`: a delegated click handler on `[data-passkey]` that runs the WebAuthn JSON API flow (`window.ermitePasskey`; tested in Node by `jstest/passkey.test.mjs`) | written here | | | |
| 15 | 15 | | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | | | |
| 16 | 16 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | |
| @@ -10,7 +10,7 @@ content-hashed names. Nothing is fetched at runtime. | |||||
| 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | ||
| 11 | | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | | | 11 | | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | | | ||
| 12 | | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | | | 12 | | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | | | ||
| 13 | - | | `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | | | 13 | + | | `static/js/files.js` | the Files view: a tree whose directories open in place and walk by keyboard (`window.ermiteTree`, tested in Node by `jstest/tree.test.mjs`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | | |
| 14 | | `static/js/passkey.js` | passkey sign-in on `/-/login` and passkey registration on `/-/passkeys`: a delegated click handler on `[data-passkey]` that runs the WebAuthn JSON API flow (`window.ermitePasskey`; tested in Node by `jstest/passkey.test.mjs`) | written here | | | 14 | | `static/js/passkey.js` | passkey sign-in on `/-/login` and passkey registration on `/-/passkeys`: a delegated click handler on `[data-passkey]` that runs the WebAuthn JSON API flow (`window.ermitePasskey`; tested in Node by `jstest/passkey.test.mjs`) | written here | | | ||
| 15 | | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | | | 15 | | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | | | ||
| 16 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | 16 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | ||
M internal/adapters/web/jstest/dom.mjs +8 -2
| @@ -143,11 +143,15 @@ export class El { | |||
| 143 | 143 | return null; | |
| 144 | 144 | } | |
| 145 | 145 | scrollIntoView() {} | |
| 146 | - | focus() { dom.focused.push(this); } | |
| 146 | + | focus() { dom.focused.push(this); dom.active = this; } | |
| 147 | + | contains(n) { | |
| 148 | + | for (let e = n; e; e = e.parent) if (e === this) return true; | |
| 149 | + | return false; | |
| 150 | + | } | |
| 147 | 151 | click() { dom.clicked.push(this); } | |
| 148 | 152 | } | |
| 149 | 153 | ||
| 150 | - | export const dom = { clicked: [], focused: [], body: null, holdFrames: false, frames: new Map(), frameID: 0 }; | |
| 154 | + | export const dom = { clicked: [], focused: [], active: null, body: null, holdFrames: false, frames: new Map(), frameID: 0 }; | |
| 151 | 155 | ||
| 152 | 156 | // flushFrames runs the frames held since the last flush. | |
| 153 | 157 | export function flushFrames() { | |
| @@ -162,8 +166,10 @@ export function installDom({ parseHTML } = {}) { | |||
| 162 | 166 | dom.body = new El('body'); | |
| 163 | 167 | dom.clicked = []; | |
| 164 | 168 | dom.focused = []; | |
| 169 | + | dom.active = null; | |
| 165 | 170 | const doc = { | |
| 166 | 171 | body: dom.body, | |
| 172 | + | get activeElement() { return dom.active || dom.body; }, | |
| 167 | 173 | createElement(tag) { | |
| 168 | 174 | const e = new El(tag); | |
| 169 | 175 | if (tag === 'template') { | |
| @@ -143,11 +143,15 @@ export class El { | |||||
| 143 | return null; | 143 | return null; | ||
| 144 | } | 144 | } | ||
| 145 | scrollIntoView() {} | 145 | scrollIntoView() {} | ||
| 146 | - | focus() { dom.focused.push(this); } | 146 | + | focus() { dom.focused.push(this); dom.active = this; } |
| 147 | + | contains(n) { | |||
| 148 | + | for (let e = n; e; e = e.parent) if (e === this) return true; | |||
| 149 | + | return false; | |||
| 150 | + | } | |||
| 147 | click() { dom.clicked.push(this); } | 151 | click() { dom.clicked.push(this); } | ||
| 148 | } | 152 | } | ||
| 149 | 153 | ||||
| 150 | - | export const dom = { clicked: [], focused: [], body: null, holdFrames: false, frames: new Map(), frameID: 0 }; | 154 | + | export const dom = { clicked: [], focused: [], active: null, body: null, holdFrames: false, frames: new Map(), frameID: 0 }; |
| 151 | 155 | ||||
| 152 | // flushFrames runs the frames held since the last flush. | 156 | // flushFrames runs the frames held since the last flush. | ||
| 153 | export function flushFrames() { | 157 | export function flushFrames() { | ||
| @@ -162,8 +166,10 @@ export function installDom({ parseHTML } = {}) { | |||||
| 162 | dom.body = new El('body'); | 166 | dom.body = new El('body'); | ||
| 163 | dom.clicked = []; | 167 | dom.clicked = []; | ||
| 164 | dom.focused = []; | 168 | dom.focused = []; | ||
| 169 | + | dom.active = null; | |||
| 165 | const doc = { | 170 | const doc = { | ||
| 166 | body: dom.body, | 171 | body: dom.body, | ||
| 172 | + | get activeElement() { return dom.active || dom.body; }, | |||
| 167 | createElement(tag) { | 173 | createElement(tag) { | ||
| 168 | const e = new El(tag); | 174 | const e = new El(tag); | ||
| 169 | if (tag === 'template') { | 175 | if (tag === 'template') { | ||
M internal/adapters/web/jstest/finder.test.mjs +3 -3
| @@ -30,13 +30,13 @@ globalThis.fetch = async () => { | |||
| 30 | 30 | }; | |
| 31 | 31 | ||
| 32 | 32 | // page builds the parts of the Files view and of the palette that files.js | |
| 33 | - | // reaches, around a query field and a tree of three rows. | |
| 33 | + | // reaches, around a query field and a tree of three rows (li). | |
| 34 | 34 | function page() { | |
| 35 | - | const rows = ['r1', 'r2', 'r3'].map((n) => el('tr', { attrs: { 'data-nm': n } })); | |
| 35 | + | const rows = ['r1', 'r2', 'r3'].map((n) => el('li', { attrs: { 'data-nm': n } })); | |
| 36 | 36 | rows[1].setAttribute('data-cursor', ''); | |
| 37 | 37 | const q = el('input', { id: 'finder-q' }); | |
| 38 | 38 | const cmdQ = el('input', { id: 'cmd-q' }); | |
| 39 | - | const treeRows = el('tbody', { id: 'tree-rows', attrs: { 'data-rows': '' } }, ...rows); | |
| 39 | + | const treeRows = el('ul', { id: 'tree-rows', attrs: { 'data-rows': '' } }, ...rows); | |
| 40 | 40 | const finderList = el('ul', { id: 'finder-list' }); | |
| 41 | 41 | const cmdList = el('ul', { id: 'cmd-list' }, el('li', { attrs: { 'data-k': 'section' }, cls: 'entry' })); | |
| 42 | 42 | const finder = el('div', { id: 'finder', hidden: true }, el('p', { id: 'finder-status' }), finderList); | |
| @@ -30,13 +30,13 @@ globalThis.fetch = async () => { | |||||
| 30 | }; | 30 | }; | ||
| 31 | 31 | ||||
| 32 | // page builds the parts of the Files view and of the palette that files.js | 32 | // page builds the parts of the Files view and of the palette that files.js | ||
| 33 | - | // reaches, around a query field and a tree of three rows. | 33 | + | // reaches, around a query field and a tree of three rows (li). |
| 34 | function page() { | 34 | function page() { | ||
| 35 | - | const rows = ['r1', 'r2', 'r3'].map((n) => el('tr', { attrs: { 'data-nm': n } })); | 35 | + | const rows = ['r1', 'r2', 'r3'].map((n) => el('li', { attrs: { 'data-nm': n } })); |
| 36 | rows[1].setAttribute('data-cursor', ''); | 36 | rows[1].setAttribute('data-cursor', ''); | ||
| 37 | const q = el('input', { id: 'finder-q' }); | 37 | const q = el('input', { id: 'finder-q' }); | ||
| 38 | const cmdQ = el('input', { id: 'cmd-q' }); | 38 | const cmdQ = el('input', { id: 'cmd-q' }); | ||
| 39 | - | const treeRows = el('tbody', { id: 'tree-rows', attrs: { 'data-rows': '' } }, ...rows); | 39 | + | const treeRows = el('ul', { id: 'tree-rows', attrs: { 'data-rows': '' } }, ...rows); |
| 40 | const finderList = el('ul', { id: 'finder-list' }); | 40 | const finderList = el('ul', { id: 'finder-list' }); | ||
| 41 | const cmdList = el('ul', { id: 'cmd-list' }, el('li', { attrs: { 'data-k': 'section' }, cls: 'entry' })); | 41 | const cmdList = el('ul', { id: 'cmd-list' }, el('li', { attrs: { 'data-k': 'section' }, cls: 'entry' })); | ||
| 42 | const finder = el('div', { id: 'finder', hidden: true }, el('p', { id: 'finder-status' }), finderList); | 42 | const finder = el('div', { id: 'finder', hidden: true }, el('p', { id: 'finder-status' }), finderList); | ||
M internal/adapters/web/jstest/tree.test.mjs +308 -17
| @@ -3,18 +3,26 @@ | |||
| 3 | 3 | // node --test 'internal/adapters/web/jstest/*.test.mjs' | |
| 4 | 4 | import assert from 'node:assert/strict'; | |
| 5 | 5 | import { test } from 'node:test'; | |
| 6 | - | import { El, el, installDom } from './dom.mjs'; | |
| 6 | + | import { dom, El, el, installDom } from './dom.mjs'; | |
| 7 | 7 | ||
| 8 | - | // A row is a name: a trailing "/" makes a directory with a link, "!x" a div | |
| 9 | - | // that is not a row, "more" the tree-more row of a truncated directory. | |
| 8 | + | // A row is a name: a trailing "/" makes a directory, "!x" a div that is not a | |
| 9 | + | // row, "more" the tree-more row of a truncated directory. | |
| 10 | 10 | function row(name, depth) { | |
| 11 | - | const tr = el('tr', { cls: name === 'more' ? 'tree-more' : '', attrs: { 'data-depth': depth } }); | |
| 12 | - | tr.name = name; | |
| 13 | - | tr.append(el('td', { cls: 'c-kind', text: '▸' })); | |
| 14 | - | if (name.endsWith('/')) { | |
| 15 | - | tr.append(el('a', { href: '/tree/' + name, attrs: { 'data-expand': '/rows/' + name, 'aria-expanded': 'false' } })); | |
| 11 | + | const dir = name.endsWith('/'); | |
| 12 | + | const li = el('li', { cls: name === 'more' ? 'tree-more' : '', attrs: { 'data-depth': depth } }); | |
| 13 | + | li.name = name; | |
| 14 | + | if (dir) { | |
| 15 | + | li.setAttribute('data-expand', '/rows/' + name); | |
| 16 | + | li.setAttribute('aria-expanded', 'false'); | |
| 16 | 17 | } | |
| 17 | - | return tr; | |
| 18 | + | li.setAttribute('role', 'treeitem'); | |
| 19 | + | const main = el('span', { cls: 't-main' }, el('span', { cls: 't-node' })); | |
| 20 | + | const a = el('a', { cls: 't-name', href: (dir ? '/tree/' : '/blob/') + name }); | |
| 21 | + | a.click = () => { globalThis.location.href = a.href; }; | |
| 22 | + | main.append(a); | |
| 23 | + | li.append(main); | |
| 24 | + | li.append(el('span', { cls: 't-sub' }, el('a', { cls: 't-commit', href: '/commit/' + name }))); | |
| 25 | + | return li; | |
| 18 | 26 | } | |
| 19 | 27 | ||
| 20 | 28 | installDom({ | |
| @@ -35,16 +43,21 @@ const answer = async (url, text, ok) => { | |||
| 35 | 43 | ||
| 36 | 44 | // click sends a click on the link of tr; it answers whether the page's own | |
| 37 | 45 | // navigation was kept (no preventDefault). | |
| 38 | - | const click = (tr, mods = {}) => { | |
| 39 | - | const a = tr.querySelector('a[data-expand]'); | |
| 46 | + | const click = (tr, mods = {}, target = tr.querySelector('a.t-name')) => { | |
| 40 | 47 | let prevented = false; | |
| 41 | - | T.click({ target: a, button: 0, preventDefault() { prevented = true; }, ...mods }); | |
| 48 | + | T.click({ target, button: 0, preventDefault() { prevented = true; }, ...mods }); | |
| 42 | 49 | return !prevented; | |
| 43 | 50 | }; | |
| 44 | - | const kindOf = (tr) => tr.querySelector('.c-kind').textContent; | |
| 51 | + | const isOpen = (tr) => tr.getAttribute('aria-expanded') === 'true'; | |
| 45 | 52 | ||
| 53 | + | // table builds the page: the list of rows, with the cursor on the first. | |
| 46 | 54 | function table(...names) { | |
| 47 | - | return el('tbody', {}, ...names.map((n) => row(n, 0))); | |
| 55 | + | const ul = el('ul', { id: 'tree-rows', attrs: { 'data-rows': '' } }, ...names.map((n) => row(n, 0))); | |
| 56 | + | dom.body.replaceChildren(ul); | |
| 57 | + | ul.children[0]?.setAttribute('data-cursor', ''); | |
| 58 | + | T.init(); | |
| 59 | + | dom.active = null; | |
| 60 | + | return ul; | |
| 48 | 61 | } | |
| 49 | 62 | const byName = (body, n) => body.children.find((r) => r.name === n); | |
| 50 | 63 | const shown = (body) => body.children.filter((r) => !r.hidden).map((r) => r.name); | |
| @@ -56,7 +69,7 @@ test('a directory opens in place and closes again without a request', async () = | |||
| 56 | 69 | await answer('/rows/P/', 'a,b'); | |
| 57 | 70 | assert.deepEqual(shown(body), ['P/', 'a', 'b', 'f']); | |
| 58 | 71 | assert.equal(byName(body, 'a').dataset.depth, '1'); | |
| 59 | - | assert.equal(kindOf(body.children[0]), '▾'); | |
| 72 | + | assert.ok(isOpen(body.children[0])); | |
| 60 | 73 | click(body.children[0]); | |
| 61 | 74 | assert.deepEqual(shown(body), ['P/', 'f']); | |
| 62 | 75 | pending.clear(); | |
| @@ -85,8 +98,8 @@ test('TS-001/TS-002: a directory that loads under a closed ancestor comes in hid | |||
| 85 | 98 | assert.deepEqual(body.children.map((r) => r.name), ['P/', 'C/', 'c1', 'c2', 'x', 'f']); | |
| 86 | 99 | assert.equal(byName(body, 'c1').hidden, true); | |
| 87 | 100 | assert.equal(C.getAttribute('aria-busy'), null); | |
| 88 | - | assert.equal(C.querySelector('a[data-expand]').getAttribute('aria-expanded'), 'true', 'C is open and loaded'); | |
| 89 | - | assert.equal(kindOf(C), '▾'); | |
| 101 | + | assert.ok(isOpen(C), 'C is open and loaded'); | |
| 102 | + | assert.ok(isOpen(C)); | |
| 90 | 103 | ||
| 91 | 104 | pending.clear(); | |
| 92 | 105 | click(P); // P opens: C's rows show with it | |
| @@ -175,3 +188,281 @@ test('the tree-more row has a depth and follows its directory', async () => { | |||
| 175 | 188 | click(body.children[0]); | |
| 176 | 189 | assert.equal(more.hidden, false, 'shown with it again'); | |
| 177 | 190 | }); | |
| 191 | + | ||
| 192 | + | // ---- the keyboard: l, h and Enter on the row under the cursor ---------------- | |
| 193 | + | ||
| 194 | + | const cursorName = (body) => body.children.find((r) => r.hasAttribute('data-cursor'))?.name; | |
| 195 | + | const nav = () => globalThis.location.href; | |
| 196 | + | ||
| 197 | + | test('l opens a closed directory, then enters it; h closes it, or goes to its directory', async () => { | |
| 198 | + | pending.clear(); | |
| 199 | + | const body = table('P/', 'f'); | |
| 200 | + | assert.equal(T.key(true), true); | |
| 201 | + | await answer('/rows/P/', 'C/,a'); | |
| 202 | + | assert.deepEqual(shown(body), ['P/', 'C/', 'a', 'f']); | |
| 203 | + | assert.equal(cursorName(body), 'P/', 'the cursor stays on the directory it opened'); | |
| 204 | + | T.key(true); // open: into its first entry | |
| 205 | + | assert.equal(cursorName(body), 'C/'); | |
| 206 | + | T.key(false); // C/ is closed: go to its directory | |
| 207 | + | assert.equal(cursorName(body), 'P/'); | |
| 208 | + | T.key(false); // P/ is open: close it | |
| 209 | + | assert.deepEqual(shown(body), ['P/', 'f']); | |
| 210 | + | assert.ok(!isOpen(body.children[0])); | |
| 211 | + | assert.equal(cursorName(body), 'P/'); | |
| 212 | + | }); | |
| 213 | + | ||
| 214 | + | test('l on a file opens it; Enter opens a file and toggles a directory', async () => { | |
| 215 | + | pending.clear(); | |
| 216 | + | const body = table('f', 'P/'); | |
| 217 | + | globalThis.location.href = ''; | |
| 218 | + | assert.equal(T.key(true), true); | |
| 219 | + | assert.equal(nav(), '/blob/f'); | |
| 220 | + | body.children[0].removeAttribute('data-cursor'); | |
| 221 | + | body.children[1].setAttribute('data-cursor', ''); | |
| 222 | + | assert.equal(T.open(), true); | |
| 223 | + | await answer('/rows/P/', 'x'); | |
| 224 | + | assert.deepEqual(shown(body), ['f', 'P/', 'x']); | |
| 225 | + | assert.equal(T.open(), true); | |
| 226 | + | assert.deepEqual(shown(body), ['f', 'P/']); | |
| 227 | + | }); | |
| 228 | + | ||
| 229 | + | test('h on a top-level row goes to the parent page, when there is one', async () => { | |
| 230 | + | pending.clear(); | |
| 231 | + | const body = table('f'); | |
| 232 | + | globalThis.location.href = ''; | |
| 233 | + | T.key(false); | |
| 234 | + | assert.equal(nav(), '', 'at the root there is nowhere to go'); | |
| 235 | + | body.setAttribute('data-up', '/tree/main'); | |
| 236 | + | T.key(false); | |
| 237 | + | assert.equal(nav(), '/tree/main'); | |
| 238 | + | }); | |
| 239 | + | ||
| 240 | + | test('the keys are not the tree\'s while the finder shows its results', async () => { | |
| 241 | + | pending.clear(); | |
| 242 | + | const body = table('P/', 'f'); | |
| 243 | + | body.removeAttribute('data-rows'); | |
| 244 | + | assert.equal(T.active(), false); | |
| 245 | + | assert.equal(T.key(true), false); | |
| 246 | + | assert.equal(T.open(), false); | |
| 247 | + | assert.deepEqual(pending.size, 0); | |
| 248 | + | }); | |
| 249 | + | ||
| 250 | + | test('a click on the name puts the cursor on the row', async () => { | |
| 251 | + | pending.clear(); | |
| 252 | + | const body = table('a', 'b'); | |
| 253 | + | click(body.children[1], {}, body.children[1].querySelector('a.t-name')); | |
| 254 | + | assert.equal(cursorName(body), 'b'); | |
| 255 | + | assert.equal(body.children[0].hasAttribute('data-cursor'), false); | |
| 256 | + | // the click on the last commit is the link's own, and the row takes the cursor | |
| 257 | + | let prevented = false; | |
| 258 | + | const sub = body.children[0].querySelector('a.t-commit'); | |
| 259 | + | T.click({ target: sub, button: 0, preventDefault() { prevented = true; } }); | |
| 260 | + | assert.equal(prevented, false); | |
| 261 | + | assert.equal(cursorName(body), 'a', 'the cursor follows the click on the row, whatever the target'); | |
| 262 | + | }); | |
| 263 | + | ||
| 264 | + | // ---- focus: one model, a roving tabindex on the rows --------------------------- | |
| 265 | + | ||
| 266 | + | const stops = (body) => body.children.filter((r) => r.getAttribute('tabindex') === '0').map((r) => r.name); | |
| 267 | + | const subTab = (r) => r.querySelector('a.t-commit').getAttribute('tabindex'); | |
| 268 | + | ||
| 269 | + | test('init: one tab stop, the row under the cursor; name links are out of the tab order', () => { | |
| 270 | + | const body = table('a', 'P/', 'b'); | |
| 271 | + | assert.deepEqual(stops(body), ['a']); | |
| 272 | + | for (const r of body.children) { | |
| 273 | + | assert.equal(r.querySelector('a.t-name').getAttribute('tabindex'), '-1', 'the row stands for its name'); | |
| 274 | + | assert.equal(subTab(r), r.name === 'a' ? '0' : '-1', 'the commit link follows its row'); | |
| 275 | + | } | |
| 276 | + | assert.equal(body.children[1].getAttribute('tabindex'), '-1'); | |
| 277 | + | }); | |
| 278 | + | ||
| 279 | + | test('l and h move the focus with the cursor, and the tab stop with them', async () => { | |
| 280 | + | pending.clear(); | |
| 281 | + | const body = table('P/', 'f'); | |
| 282 | + | T.key(true); | |
| 283 | + | await answer('/rows/P/', 'C/,a'); | |
| 284 | + | const C = byName(body, 'C/'); | |
| 285 | + | assert.equal(C.getAttribute('tabindex'), '-1', 'rows that come in are out of the tab order'); | |
| 286 | + | assert.equal(C.querySelector('a.t-name').getAttribute('tabindex'), '-1'); | |
| 287 | + | assert.equal(subTab(C), '-1'); | |
| 288 | + | T.key(true); // into the first entry | |
| 289 | + | assert.equal(dom.active, C); | |
| 290 | + | assert.deepEqual(stops(body), ['C/']); | |
| 291 | + | assert.equal(subTab(C), '0'); | |
| 292 | + | assert.equal(subTab(body.children[0]), '-1'); | |
| 293 | + | T.key(false); // up to P/ | |
| 294 | + | assert.equal(dom.active, body.children[0]); | |
| 295 | + | assert.deepEqual(stops(body), ['P/']); | |
| 296 | + | }); | |
| 297 | + | ||
| 298 | + | test('sync (j k gg G of ermite.js) puts the focus and the tab stop on the row', () => { | |
| 299 | + | const body = table('a', 'b', 'c'); | |
| 300 | + | body.children[0].removeAttribute('data-cursor'); | |
| 301 | + | body.children[2].setAttribute('data-cursor', ''); // ermite.js moved the cursor | |
| 302 | + | T.sync(body.children[2]); | |
| 303 | + | assert.equal(dom.active, body.children[2]); | |
| 304 | + | assert.deepEqual(stops(body), ['c']); | |
| 305 | + | assert.equal(body.children.filter((r) => r.hasAttribute('data-cursor')).length, 1); | |
| 306 | + | T.sync(el('li')); // a row of another list: not the tree's | |
| 307 | + | assert.equal(dom.active, body.children[2]); | |
| 308 | + | }); | |
| 309 | + | ||
| 310 | + | test('a click puts the focus and the tab stop on its row, directory or not', async () => { | |
| 311 | + | pending.clear(); | |
| 312 | + | const body = table('a', 'b/'); | |
| 313 | + | click(body.children[1]); | |
| 314 | + | assert.equal(dom.active, body.children[1]); | |
| 315 | + | assert.deepEqual(stops(body), ['b/']); | |
| 316 | + | await answer('/rows/b/', 'x'); | |
| 317 | + | // a click on the commit link: the row takes the focus, the link keeps its own navigation | |
| 318 | + | let prevented = false; | |
| 319 | + | T.click({ target: body.children[0].querySelector('a.t-commit'), button: 0, preventDefault() { prevented = true; } }); | |
| 320 | + | assert.equal(prevented, false); | |
| 321 | + | assert.equal(dom.active, body.children[0]); | |
| 322 | + | assert.deepEqual(stops(body), ['a']); | |
| 323 | + | // a modified click on the name too | |
| 324 | + | click(body.children[1], { ctrlKey: true }); | |
| 325 | + | assert.equal(dom.active, body.children[1]); | |
| 326 | + | }); | |
| 327 | + | ||
| 328 | + | test('collapsing a branch with the focus or the cursor in it moves both to the directory', async () => { | |
| 329 | + | pending.clear(); | |
| 330 | + | const body = table('P/', 'f'); | |
| 331 | + | T.key(true); | |
| 332 | + | await answer('/rows/P/', 'a,b'); | |
| 333 | + | const a = byName(body, 'a'); | |
| 334 | + | click(a); // the cursor and the focus on a | |
| 335 | + | assert.equal(dom.active, a); | |
| 336 | + | click(body.children[0]); // the directory closes | |
| 337 | + | assert.deepEqual(shown(body), ['P/', 'f']); | |
| 338 | + | assert.equal(dom.active, body.children[0], 'the focus leaves the hidden row'); | |
| 339 | + | assert.deepEqual(stops(body), ['P/']); | |
| 340 | + | assert.ok(body.children[0].hasAttribute('data-cursor')); | |
| 341 | + | assert.equal(a.hasAttribute('data-cursor'), false); | |
| 342 | + | assert.equal(a.getAttribute('tabindex'), '-1'); | |
| 343 | + | }); | |
| 344 | + | ||
| 345 | + | test('collapsing a directory the focus is not in leaves the focus alone', async () => { | |
| 346 | + | pending.clear(); | |
| 347 | + | const body = table('P/', 'f'); | |
| 348 | + | T.key(true); | |
| 349 | + | await answer('/rows/P/', 'a'); | |
| 350 | + | click(body.children[0]); // P/ has the cursor and the focus; closing it keeps them | |
| 351 | + | assert.equal(dom.active, body.children[0]); | |
| 352 | + | click(body.children[3] ?? body.children[2]); // f | |
| 353 | + | const f = byName(body, 'f'); | |
| 354 | + | assert.equal(dom.active, f); | |
| 355 | + | T.key(false); // f: no parent in the page, no up: nothing | |
| 356 | + | assert.equal(dom.active, f); | |
| 357 | + | }); | |
| 358 | + | ||
| 359 | + | test('Enter acts on the row that has the cursor, which is the focused row', async () => { | |
| 360 | + | pending.clear(); | |
| 361 | + | const body = table('a', 'P/'); | |
| 362 | + | T.sync(body.children[1]); // ermite.js moved the cursor and the focus to P/ | |
| 363 | + | assert.equal(dom.active, body.children[1]); | |
| 364 | + | T.open(); | |
| 365 | + | await answer('/rows/P/', 'x'); | |
| 366 | + | assert.ok(isOpen(body.children[1])); | |
| 367 | + | assert.equal(body.children.filter((r) => r.hasAttribute('data-cursor')).length, 1); | |
| 368 | + | }); | |
| 369 | + | ||
| 370 | + | // ---- ARIA: the level is set on the rows inserted; position and size come with them ---- | |
| 371 | + | ||
| 372 | + | test('inserted rows get their level; the server gave them posinset and setsize', async () => { | |
| 373 | + | pending.clear(); | |
| 374 | + | const body = table('P/', 'f'); | |
| 375 | + | T.key(true); | |
| 376 | + | await answer('/rows/P/', 'C/,a'); | |
| 377 | + | assert.equal(byName(body, 'C/').getAttribute('aria-level'), '2'); | |
| 378 | + | assert.equal(byName(body, 'C/').getAttribute('role'), 'treeitem'); | |
| 379 | + | // a directory inside it: level 3 | |
| 380 | + | click(byName(body, 'C/')); | |
| 381 | + | await answer('/rows/C/', 'z'); | |
| 382 | + | assert.equal(byName(body, 'z').getAttribute('aria-level'), '3'); | |
| 383 | + | }); | |
| 384 | + | ||
| 385 | + | // ---- the arrow keys: the APG tree pattern, apart from the Vim keys --------------- | |
| 386 | + | ||
| 387 | + | test('Down, Up, Home and End walk the visible rows, with the focus', async () => { | |
| 388 | + | pending.clear(); | |
| 389 | + | const body = table('P/', 'f', 'g'); | |
| 390 | + | T.arrow('ArrowRight'); | |
| 391 | + | await answer('/rows/P/', 'a,b'); | |
| 392 | + | T.arrow('ArrowRight'); // open: into the first entry | |
| 393 | + | assert.equal(cursorName(body), 'a'); | |
| 394 | + | T.arrow('ArrowDown'); | |
| 395 | + | assert.equal(cursorName(body), 'b'); | |
| 396 | + | assert.equal(dom.active, byName(body, 'b')); | |
| 397 | + | T.arrow('ArrowUp'); | |
| 398 | + | assert.equal(cursorName(body), 'a'); | |
| 399 | + | T.arrow('End'); | |
| 400 | + | assert.equal(cursorName(body), 'g'); | |
| 401 | + | T.arrow('ArrowDown'); // the last row: stays | |
| 402 | + | assert.equal(cursorName(body), 'g'); | |
| 403 | + | T.arrow('Home'); | |
| 404 | + | assert.equal(cursorName(body), 'P/'); | |
| 405 | + | T.arrow('ArrowUp'); | |
| 406 | + | assert.equal(cursorName(body), 'P/'); | |
| 407 | + | // a collapsed branch is skipped | |
| 408 | + | T.arrow('ArrowLeft'); | |
| 409 | + | T.arrow('ArrowDown'); | |
| 410 | + | assert.equal(cursorName(body), 'f'); | |
| 411 | + | assert.deepEqual(stops(body), ['f']); | |
| 412 | + | }); | |
| 413 | + | ||
| 414 | + | test('Right opens a closed directory, enters an open one, and does nothing on a file', async () => { | |
| 415 | + | pending.clear(); | |
| 416 | + | globalThis.location.href = ''; | |
| 417 | + | const body = table('P/', 'f'); | |
| 418 | + | assert.equal(T.arrow('ArrowRight'), true); | |
| 419 | + | await answer('/rows/P/', 'a'); | |
| 420 | + | assert.ok(isOpen(body.children[0])); | |
| 421 | + | assert.equal(cursorName(body), 'P/', 'opening keeps the cursor on the directory'); | |
| 422 | + | T.arrow('ArrowRight'); | |
| 423 | + | assert.equal(cursorName(body), 'a', 'on an open directory: the first entry'); | |
| 424 | + | T.arrow('ArrowRight'); // a file: nothing, in particular it is not opened | |
| 425 | + | assert.equal(cursorName(body), 'a'); | |
| 426 | + | assert.equal(nav(), '', 'a file is not opened by the arrow'); | |
| 427 | + | assert.equal(pending.size, 1, 'no further request'); | |
| 428 | + | }); | |
| 429 | + | ||
| 430 | + | test('Left closes an open directory, goes to the parent, and does nothing at the top', async () => { | |
| 431 | + | pending.clear(); | |
| 432 | + | globalThis.location.href = ''; | |
| 433 | + | const body = table('P/', 'f'); | |
| 434 | + | body.setAttribute('data-up', '/tree/main'); // a page to go up to: not for an arrow | |
| 435 | + | T.arrow('ArrowRight'); | |
| 436 | + | await answer('/rows/P/', 'a'); | |
| 437 | + | T.arrow('ArrowRight'); | |
| 438 | + | T.arrow('ArrowLeft'); // a: to its directory | |
| 439 | + | assert.equal(cursorName(body), 'P/'); | |
| 440 | + | T.arrow('ArrowLeft'); // P/ is open: closes | |
| 441 | + | assert.deepEqual(shown(body), ['P/', 'f']); | |
| 442 | + | T.arrow('ArrowLeft'); // closed and top level: nothing | |
| 443 | + | assert.equal(cursorName(body), 'P/'); | |
| 444 | + | assert.equal(nav(), '', 'the arrow does not leave the page; h does'); | |
| 445 | + | T.key(false); | |
| 446 | + | assert.equal(nav(), '/tree/main'); | |
| 447 | + | }); | |
| 448 | + | ||
| 449 | + | test('l opens a file where Right does not', () => { | |
| 450 | + | pending.clear(); | |
| 451 | + | globalThis.location.href = ''; | |
| 452 | + | const body = table('f'); | |
| 453 | + | T.arrow('ArrowRight'); | |
| 454 | + | assert.equal(nav(), ''); | |
| 455 | + | T.key(true); | |
| 456 | + | assert.equal(nav(), '/blob/f'); | |
| 457 | + | assert.ok(body); | |
| 458 | + | }); | |
| 459 | + | ||
| 460 | + | test('the tree takes no arrow key while the finder shows its results, nor any other key', () => { | |
| 461 | + | const body = table('a', 'b'); | |
| 462 | + | body.removeAttribute('data-rows'); | |
| 463 | + | assert.equal(T.arrow('ArrowDown'), false); | |
| 464 | + | assert.equal(cursorName(body), 'a'); | |
| 465 | + | body.setAttribute('data-rows', ''); | |
| 466 | + | assert.equal(T.arrow('x'), false); | |
| 467 | + | assert.equal(T.arrow('PageDown'), false); | |
| 468 | + | }); | |
| @@ -3,18 +3,26 @@ | |||||
| 3 | // node --test 'internal/adapters/web/jstest/*.test.mjs' | 3 | // node --test 'internal/adapters/web/jstest/*.test.mjs' | ||
| 4 | import assert from 'node:assert/strict'; | 4 | import assert from 'node:assert/strict'; | ||
| 5 | import { test } from 'node:test'; | 5 | import { test } from 'node:test'; | ||
| 6 | - | import { El, el, installDom } from './dom.mjs'; | 6 | + | import { dom, El, el, installDom } from './dom.mjs'; |
| 7 | 7 | ||||
| 8 | - | // A row is a name: a trailing "/" makes a directory with a link, "!x" a div | 8 | + | // A row is a name: a trailing "/" makes a directory, "!x" a div that is not a |
| 9 | - | // that is not a row, "more" the tree-more row of a truncated directory. | 9 | + | // row, "more" the tree-more row of a truncated directory. |
| 10 | function row(name, depth) { | 10 | function row(name, depth) { | ||
| 11 | - | const tr = el('tr', { cls: name === 'more' ? 'tree-more' : '', attrs: { 'data-depth': depth } }); | 11 | + | const dir = name.endsWith('/'); |
| 12 | - | tr.name = name; | 12 | + | const li = el('li', { cls: name === 'more' ? 'tree-more' : '', attrs: { 'data-depth': depth } }); |
| 13 | - | tr.append(el('td', { cls: 'c-kind', text: '▸' })); | 13 | + | li.name = name; |
| 14 | - | if (name.endsWith('/')) { | 14 | + | if (dir) { |
| 15 | - | tr.append(el('a', { href: '/tree/' + name, attrs: { 'data-expand': '/rows/' + name, 'aria-expanded': 'false' } })); | 15 | + | li.setAttribute('data-expand', '/rows/' + name); |
| 16 | + | li.setAttribute('aria-expanded', 'false'); | |||
| 16 | } | 17 | } | ||
| 17 | - | return tr; | 18 | + | li.setAttribute('role', 'treeitem'); |
| 19 | + | const main = el('span', { cls: 't-main' }, el('span', { cls: 't-node' })); | |||
| 20 | + | const a = el('a', { cls: 't-name', href: (dir ? '/tree/' : '/blob/') + name }); | |||
| 21 | + | a.click = () => { globalThis.location.href = a.href; }; | |||
| 22 | + | main.append(a); | |||
| 23 | + | li.append(main); | |||
| 24 | + | li.append(el('span', { cls: 't-sub' }, el('a', { cls: 't-commit', href: '/commit/' + name }))); | |||
| 25 | + | return li; | |||
| 18 | } | 26 | } | ||
| 19 | 27 | ||||
| 20 | installDom({ | 28 | installDom({ | ||
| @@ -35,16 +43,21 @@ const answer = async (url, text, ok) => { | |||||
| 35 | 43 | ||||
| 36 | // click sends a click on the link of tr; it answers whether the page's own | 44 | // click sends a click on the link of tr; it answers whether the page's own | ||
| 37 | // navigation was kept (no preventDefault). | 45 | // navigation was kept (no preventDefault). | ||
| 38 | - | const click = (tr, mods = {}) => { | 46 | + | const click = (tr, mods = {}, target = tr.querySelector('a.t-name')) => { |
| 39 | - | const a = tr.querySelector('a[data-expand]'); | |||
| 40 | let prevented = false; | 47 | let prevented = false; | ||
| 41 | - | T.click({ target: a, button: 0, preventDefault() { prevented = true; }, ...mods }); | 48 | + | T.click({ target, button: 0, preventDefault() { prevented = true; }, ...mods }); |
| 42 | return !prevented; | 49 | return !prevented; | ||
| 43 | }; | 50 | }; | ||
| 44 | - | const kindOf = (tr) => tr.querySelector('.c-kind').textContent; | 51 | + | const isOpen = (tr) => tr.getAttribute('aria-expanded') === 'true'; |
| 45 | 52 | ||||
| 53 | + | // table builds the page: the list of rows, with the cursor on the first. | |||
| 46 | function table(...names) { | 54 | function table(...names) { | ||
| 47 | - | return el('tbody', {}, ...names.map((n) => row(n, 0))); | 55 | + | const ul = el('ul', { id: 'tree-rows', attrs: { 'data-rows': '' } }, ...names.map((n) => row(n, 0))); |
| 56 | + | dom.body.replaceChildren(ul); | |||
| 57 | + | ul.children[0]?.setAttribute('data-cursor', ''); | |||
| 58 | + | T.init(); | |||
| 59 | + | dom.active = null; | |||
| 60 | + | return ul; | |||
| 48 | } | 61 | } | ||
| 49 | const byName = (body, n) => body.children.find((r) => r.name === n); | 62 | const byName = (body, n) => body.children.find((r) => r.name === n); | ||
| 50 | const shown = (body) => body.children.filter((r) => !r.hidden).map((r) => r.name); | 63 | const shown = (body) => body.children.filter((r) => !r.hidden).map((r) => r.name); | ||
| @@ -56,7 +69,7 @@ test('a directory opens in place and closes again without a request', async () = | |||||
| 56 | await answer('/rows/P/', 'a,b'); | 69 | await answer('/rows/P/', 'a,b'); | ||
| 57 | assert.deepEqual(shown(body), ['P/', 'a', 'b', 'f']); | 70 | assert.deepEqual(shown(body), ['P/', 'a', 'b', 'f']); | ||
| 58 | assert.equal(byName(body, 'a').dataset.depth, '1'); | 71 | assert.equal(byName(body, 'a').dataset.depth, '1'); | ||
| 59 | - | assert.equal(kindOf(body.children[0]), '▾'); | 72 | + | assert.ok(isOpen(body.children[0])); |
| 60 | click(body.children[0]); | 73 | click(body.children[0]); | ||
| 61 | assert.deepEqual(shown(body), ['P/', 'f']); | 74 | assert.deepEqual(shown(body), ['P/', 'f']); | ||
| 62 | pending.clear(); | 75 | pending.clear(); | ||
| @@ -85,8 +98,8 @@ test('TS-001/TS-002: a directory that loads under a closed ancestor comes in hid | |||||
| 85 | assert.deepEqual(body.children.map((r) => r.name), ['P/', 'C/', 'c1', 'c2', 'x', 'f']); | 98 | assert.deepEqual(body.children.map((r) => r.name), ['P/', 'C/', 'c1', 'c2', 'x', 'f']); | ||
| 86 | assert.equal(byName(body, 'c1').hidden, true); | 99 | assert.equal(byName(body, 'c1').hidden, true); | ||
| 87 | assert.equal(C.getAttribute('aria-busy'), null); | 100 | assert.equal(C.getAttribute('aria-busy'), null); | ||
| 88 | - | assert.equal(C.querySelector('a[data-expand]').getAttribute('aria-expanded'), 'true', 'C is open and loaded'); | 101 | + | assert.ok(isOpen(C), 'C is open and loaded'); |
| 89 | - | assert.equal(kindOf(C), '▾'); | 102 | + | assert.ok(isOpen(C)); |
| 90 | 103 | ||||
| 91 | pending.clear(); | 104 | pending.clear(); | ||
| 92 | click(P); // P opens: C's rows show with it | 105 | click(P); // P opens: C's rows show with it | ||
| @@ -175,3 +188,281 @@ test('the tree-more row has a depth and follows its directory', async () => { | |||||
| 175 | click(body.children[0]); | 188 | click(body.children[0]); | ||
| 176 | assert.equal(more.hidden, false, 'shown with it again'); | 189 | assert.equal(more.hidden, false, 'shown with it again'); | ||
| 177 | }); | 190 | }); | ||
| 191 | + | ||||
| 192 | + | // ---- the keyboard: l, h and Enter on the row under the cursor ---------------- | |||
| 193 | + | ||||
| 194 | + | const cursorName = (body) => body.children.find((r) => r.hasAttribute('data-cursor'))?.name; | |||
| 195 | + | const nav = () => globalThis.location.href; | |||
| 196 | + | ||||
| 197 | + | test('l opens a closed directory, then enters it; h closes it, or goes to its directory', async () => { | |||
| 198 | + | pending.clear(); | |||
| 199 | + | const body = table('P/', 'f'); | |||
| 200 | + | assert.equal(T.key(true), true); | |||
| 201 | + | await answer('/rows/P/', 'C/,a'); | |||
| 202 | + | assert.deepEqual(shown(body), ['P/', 'C/', 'a', 'f']); | |||
| 203 | + | assert.equal(cursorName(body), 'P/', 'the cursor stays on the directory it opened'); | |||
| 204 | + | T.key(true); // open: into its first entry | |||
| 205 | + | assert.equal(cursorName(body), 'C/'); | |||
| 206 | + | T.key(false); // C/ is closed: go to its directory | |||
| 207 | + | assert.equal(cursorName(body), 'P/'); | |||
| 208 | + | T.key(false); // P/ is open: close it | |||
| 209 | + | assert.deepEqual(shown(body), ['P/', 'f']); | |||
| 210 | + | assert.ok(!isOpen(body.children[0])); | |||
| 211 | + | assert.equal(cursorName(body), 'P/'); | |||
| 212 | + | }); | |||
| 213 | + | ||||
| 214 | + | test('l on a file opens it; Enter opens a file and toggles a directory', async () => { | |||
| 215 | + | pending.clear(); | |||
| 216 | + | const body = table('f', 'P/'); | |||
| 217 | + | globalThis.location.href = ''; | |||
| 218 | + | assert.equal(T.key(true), true); | |||
| 219 | + | assert.equal(nav(), '/blob/f'); | |||
| 220 | + | body.children[0].removeAttribute('data-cursor'); | |||
| 221 | + | body.children[1].setAttribute('data-cursor', ''); | |||
| 222 | + | assert.equal(T.open(), true); | |||
| 223 | + | await answer('/rows/P/', 'x'); | |||
| 224 | + | assert.deepEqual(shown(body), ['f', 'P/', 'x']); | |||
| 225 | + | assert.equal(T.open(), true); | |||
| 226 | + | assert.deepEqual(shown(body), ['f', 'P/']); | |||
| 227 | + | }); | |||
| 228 | + | ||||
| 229 | + | test('h on a top-level row goes to the parent page, when there is one', async () => { | |||
| 230 | + | pending.clear(); | |||
| 231 | + | const body = table('f'); | |||
| 232 | + | globalThis.location.href = ''; | |||
| 233 | + | T.key(false); | |||
| 234 | + | assert.equal(nav(), '', 'at the root there is nowhere to go'); | |||
| 235 | + | body.setAttribute('data-up', '/tree/main'); | |||
| 236 | + | T.key(false); | |||
| 237 | + | assert.equal(nav(), '/tree/main'); | |||
| 238 | + | }); | |||
| 239 | + | ||||
| 240 | + | test('the keys are not the tree\'s while the finder shows its results', async () => { | |||
| 241 | + | pending.clear(); | |||
| 242 | + | const body = table('P/', 'f'); | |||
| 243 | + | body.removeAttribute('data-rows'); | |||
| 244 | + | assert.equal(T.active(), false); | |||
| 245 | + | assert.equal(T.key(true), false); | |||
| 246 | + | assert.equal(T.open(), false); | |||
| 247 | + | assert.deepEqual(pending.size, 0); | |||
| 248 | + | }); | |||
| 249 | + | ||||
| 250 | + | test('a click on the name puts the cursor on the row', async () => { | |||
| 251 | + | pending.clear(); | |||
| 252 | + | const body = table('a', 'b'); | |||
| 253 | + | click(body.children[1], {}, body.children[1].querySelector('a.t-name')); | |||
| 254 | + | assert.equal(cursorName(body), 'b'); | |||
| 255 | + | assert.equal(body.children[0].hasAttribute('data-cursor'), false); | |||
| 256 | + | // the click on the last commit is the link's own, and the row takes the cursor | |||
| 257 | + | let prevented = false; | |||
| 258 | + | const sub = body.children[0].querySelector('a.t-commit'); | |||
| 259 | + | T.click({ target: sub, button: 0, preventDefault() { prevented = true; } }); | |||
| 260 | + | assert.equal(prevented, false); | |||
| 261 | + | assert.equal(cursorName(body), 'a', 'the cursor follows the click on the row, whatever the target'); | |||
| 262 | + | }); | |||
| 263 | + | ||||
| 264 | + | // ---- focus: one model, a roving tabindex on the rows --------------------------- | |||
| 265 | + | ||||
| 266 | + | const stops = (body) => body.children.filter((r) => r.getAttribute('tabindex') === '0').map((r) => r.name); | |||
| 267 | + | const subTab = (r) => r.querySelector('a.t-commit').getAttribute('tabindex'); | |||
| 268 | + | ||||
| 269 | + | test('init: one tab stop, the row under the cursor; name links are out of the tab order', () => { | |||
| 270 | + | const body = table('a', 'P/', 'b'); | |||
| 271 | + | assert.deepEqual(stops(body), ['a']); | |||
| 272 | + | for (const r of body.children) { | |||
| 273 | + | assert.equal(r.querySelector('a.t-name').getAttribute('tabindex'), '-1', 'the row stands for its name'); | |||
| 274 | + | assert.equal(subTab(r), r.name === 'a' ? '0' : '-1', 'the commit link follows its row'); | |||
| 275 | + | } | |||
| 276 | + | assert.equal(body.children[1].getAttribute('tabindex'), '-1'); | |||
| 277 | + | }); | |||
| 278 | + | ||||
| 279 | + | test('l and h move the focus with the cursor, and the tab stop with them', async () => { | |||
| 280 | + | pending.clear(); | |||
| 281 | + | const body = table('P/', 'f'); | |||
| 282 | + | T.key(true); | |||
| 283 | + | await answer('/rows/P/', 'C/,a'); | |||
| 284 | + | const C = byName(body, 'C/'); | |||
| 285 | + | assert.equal(C.getAttribute('tabindex'), '-1', 'rows that come in are out of the tab order'); | |||
| 286 | + | assert.equal(C.querySelector('a.t-name').getAttribute('tabindex'), '-1'); | |||
| 287 | + | assert.equal(subTab(C), '-1'); | |||
| 288 | + | T.key(true); // into the first entry | |||
| 289 | + | assert.equal(dom.active, C); | |||
| 290 | + | assert.deepEqual(stops(body), ['C/']); | |||
| 291 | + | assert.equal(subTab(C), '0'); | |||
| 292 | + | assert.equal(subTab(body.children[0]), '-1'); | |||
| 293 | + | T.key(false); // up to P/ | |||
| 294 | + | assert.equal(dom.active, body.children[0]); | |||
| 295 | + | assert.deepEqual(stops(body), ['P/']); | |||
| 296 | + | }); | |||
| 297 | + | ||||
| 298 | + | test('sync (j k gg G of ermite.js) puts the focus and the tab stop on the row', () => { | |||
| 299 | + | const body = table('a', 'b', 'c'); | |||
| 300 | + | body.children[0].removeAttribute('data-cursor'); | |||
| 301 | + | body.children[2].setAttribute('data-cursor', ''); // ermite.js moved the cursor | |||
| 302 | + | T.sync(body.children[2]); | |||
| 303 | + | assert.equal(dom.active, body.children[2]); | |||
| 304 | + | assert.deepEqual(stops(body), ['c']); | |||
| 305 | + | assert.equal(body.children.filter((r) => r.hasAttribute('data-cursor')).length, 1); | |||
| 306 | + | T.sync(el('li')); // a row of another list: not the tree's | |||
| 307 | + | assert.equal(dom.active, body.children[2]); | |||
| 308 | + | }); | |||
| 309 | + | ||||
| 310 | + | test('a click puts the focus and the tab stop on its row, directory or not', async () => { | |||
| 311 | + | pending.clear(); | |||
| 312 | + | const body = table('a', 'b/'); | |||
| 313 | + | click(body.children[1]); | |||
| 314 | + | assert.equal(dom.active, body.children[1]); | |||
| 315 | + | assert.deepEqual(stops(body), ['b/']); | |||
| 316 | + | await answer('/rows/b/', 'x'); | |||
| 317 | + | // a click on the commit link: the row takes the focus, the link keeps its own navigation | |||
| 318 | + | let prevented = false; | |||
| 319 | + | T.click({ target: body.children[0].querySelector('a.t-commit'), button: 0, preventDefault() { prevented = true; } }); | |||
| 320 | + | assert.equal(prevented, false); | |||
| 321 | + | assert.equal(dom.active, body.children[0]); | |||
| 322 | + | assert.deepEqual(stops(body), ['a']); | |||
| 323 | + | // a modified click on the name too | |||
| 324 | + | click(body.children[1], { ctrlKey: true }); | |||
| 325 | + | assert.equal(dom.active, body.children[1]); | |||
| 326 | + | }); | |||
| 327 | + | ||||
| 328 | + | test('collapsing a branch with the focus or the cursor in it moves both to the directory', async () => { | |||
| 329 | + | pending.clear(); | |||
| 330 | + | const body = table('P/', 'f'); | |||
| 331 | + | T.key(true); | |||
| 332 | + | await answer('/rows/P/', 'a,b'); | |||
| 333 | + | const a = byName(body, 'a'); | |||
| 334 | + | click(a); // the cursor and the focus on a | |||
| 335 | + | assert.equal(dom.active, a); | |||
| 336 | + | click(body.children[0]); // the directory closes | |||
| 337 | + | assert.deepEqual(shown(body), ['P/', 'f']); | |||
| 338 | + | assert.equal(dom.active, body.children[0], 'the focus leaves the hidden row'); | |||
| 339 | + | assert.deepEqual(stops(body), ['P/']); | |||
| 340 | + | assert.ok(body.children[0].hasAttribute('data-cursor')); | |||
| 341 | + | assert.equal(a.hasAttribute('data-cursor'), false); | |||
| 342 | + | assert.equal(a.getAttribute('tabindex'), '-1'); | |||
| 343 | + | }); | |||
| 344 | + | ||||
| 345 | + | test('collapsing a directory the focus is not in leaves the focus alone', async () => { | |||
| 346 | + | pending.clear(); | |||
| 347 | + | const body = table('P/', 'f'); | |||
| 348 | + | T.key(true); | |||
| 349 | + | await answer('/rows/P/', 'a'); | |||
| 350 | + | click(body.children[0]); // P/ has the cursor and the focus; closing it keeps them | |||
| 351 | + | assert.equal(dom.active, body.children[0]); | |||
| 352 | + | click(body.children[3] ?? body.children[2]); // f | |||
| 353 | + | const f = byName(body, 'f'); | |||
| 354 | + | assert.equal(dom.active, f); | |||
| 355 | + | T.key(false); // f: no parent in the page, no up: nothing | |||
| 356 | + | assert.equal(dom.active, f); | |||
| 357 | + | }); | |||
| 358 | + | ||||
| 359 | + | test('Enter acts on the row that has the cursor, which is the focused row', async () => { | |||
| 360 | + | pending.clear(); | |||
| 361 | + | const body = table('a', 'P/'); | |||
| 362 | + | T.sync(body.children[1]); // ermite.js moved the cursor and the focus to P/ | |||
| 363 | + | assert.equal(dom.active, body.children[1]); | |||
| 364 | + | T.open(); | |||
| 365 | + | await answer('/rows/P/', 'x'); | |||
| 366 | + | assert.ok(isOpen(body.children[1])); | |||
| 367 | + | assert.equal(body.children.filter((r) => r.hasAttribute('data-cursor')).length, 1); | |||
| 368 | + | }); | |||
| 369 | + | ||||
| 370 | + | // ---- ARIA: the level is set on the rows inserted; position and size come with them ---- | |||
| 371 | + | ||||
| 372 | + | test('inserted rows get their level; the server gave them posinset and setsize', async () => { | |||
| 373 | + | pending.clear(); | |||
| 374 | + | const body = table('P/', 'f'); | |||
| 375 | + | T.key(true); | |||
| 376 | + | await answer('/rows/P/', 'C/,a'); | |||
| 377 | + | assert.equal(byName(body, 'C/').getAttribute('aria-level'), '2'); | |||
| 378 | + | assert.equal(byName(body, 'C/').getAttribute('role'), 'treeitem'); | |||
| 379 | + | // a directory inside it: level 3 | |||
| 380 | + | click(byName(body, 'C/')); | |||
| 381 | + | await answer('/rows/C/', 'z'); | |||
| 382 | + | assert.equal(byName(body, 'z').getAttribute('aria-level'), '3'); | |||
| 383 | + | }); | |||
| 384 | + | ||||
| 385 | + | // ---- the arrow keys: the APG tree pattern, apart from the Vim keys --------------- | |||
| 386 | + | ||||
| 387 | + | test('Down, Up, Home and End walk the visible rows, with the focus', async () => { | |||
| 388 | + | pending.clear(); | |||
| 389 | + | const body = table('P/', 'f', 'g'); | |||
| 390 | + | T.arrow('ArrowRight'); | |||
| 391 | + | await answer('/rows/P/', 'a,b'); | |||
| 392 | + | T.arrow('ArrowRight'); // open: into the first entry | |||
| 393 | + | assert.equal(cursorName(body), 'a'); | |||
| 394 | + | T.arrow('ArrowDown'); | |||
| 395 | + | assert.equal(cursorName(body), 'b'); | |||
| 396 | + | assert.equal(dom.active, byName(body, 'b')); | |||
| 397 | + | T.arrow('ArrowUp'); | |||
| 398 | + | assert.equal(cursorName(body), 'a'); | |||
| 399 | + | T.arrow('End'); | |||
| 400 | + | assert.equal(cursorName(body), 'g'); | |||
| 401 | + | T.arrow('ArrowDown'); // the last row: stays | |||
| 402 | + | assert.equal(cursorName(body), 'g'); | |||
| 403 | + | T.arrow('Home'); | |||
| 404 | + | assert.equal(cursorName(body), 'P/'); | |||
| 405 | + | T.arrow('ArrowUp'); | |||
| 406 | + | assert.equal(cursorName(body), 'P/'); | |||
| 407 | + | // a collapsed branch is skipped | |||
| 408 | + | T.arrow('ArrowLeft'); | |||
| 409 | + | T.arrow('ArrowDown'); | |||
| 410 | + | assert.equal(cursorName(body), 'f'); | |||
| 411 | + | assert.deepEqual(stops(body), ['f']); | |||
| 412 | + | }); | |||
| 413 | + | ||||
| 414 | + | test('Right opens a closed directory, enters an open one, and does nothing on a file', async () => { | |||
| 415 | + | pending.clear(); | |||
| 416 | + | globalThis.location.href = ''; | |||
| 417 | + | const body = table('P/', 'f'); | |||
| 418 | + | assert.equal(T.arrow('ArrowRight'), true); | |||
| 419 | + | await answer('/rows/P/', 'a'); | |||
| 420 | + | assert.ok(isOpen(body.children[0])); | |||
| 421 | + | assert.equal(cursorName(body), 'P/', 'opening keeps the cursor on the directory'); | |||
| 422 | + | T.arrow('ArrowRight'); | |||
| 423 | + | assert.equal(cursorName(body), 'a', 'on an open directory: the first entry'); | |||
| 424 | + | T.arrow('ArrowRight'); // a file: nothing, in particular it is not opened | |||
| 425 | + | assert.equal(cursorName(body), 'a'); | |||
| 426 | + | assert.equal(nav(), '', 'a file is not opened by the arrow'); | |||
| 427 | + | assert.equal(pending.size, 1, 'no further request'); | |||
| 428 | + | }); | |||
| 429 | + | ||||
| 430 | + | test('Left closes an open directory, goes to the parent, and does nothing at the top', async () => { | |||
| 431 | + | pending.clear(); | |||
| 432 | + | globalThis.location.href = ''; | |||
| 433 | + | const body = table('P/', 'f'); | |||
| 434 | + | body.setAttribute('data-up', '/tree/main'); // a page to go up to: not for an arrow | |||
| 435 | + | T.arrow('ArrowRight'); | |||
| 436 | + | await answer('/rows/P/', 'a'); | |||
| 437 | + | T.arrow('ArrowRight'); | |||
| 438 | + | T.arrow('ArrowLeft'); // a: to its directory | |||
| 439 | + | assert.equal(cursorName(body), 'P/'); | |||
| 440 | + | T.arrow('ArrowLeft'); // P/ is open: closes | |||
| 441 | + | assert.deepEqual(shown(body), ['P/', 'f']); | |||
| 442 | + | T.arrow('ArrowLeft'); // closed and top level: nothing | |||
| 443 | + | assert.equal(cursorName(body), 'P/'); | |||
| 444 | + | assert.equal(nav(), '', 'the arrow does not leave the page; h does'); | |||
| 445 | + | T.key(false); | |||
| 446 | + | assert.equal(nav(), '/tree/main'); | |||
| 447 | + | }); | |||
| 448 | + | ||||
| 449 | + | test('l opens a file where Right does not', () => { | |||
| 450 | + | pending.clear(); | |||
| 451 | + | globalThis.location.href = ''; | |||
| 452 | + | const body = table('f'); | |||
| 453 | + | T.arrow('ArrowRight'); | |||
| 454 | + | assert.equal(nav(), ''); | |||
| 455 | + | T.key(true); | |||
| 456 | + | assert.equal(nav(), '/blob/f'); | |||
| 457 | + | assert.ok(body); | |||
| 458 | + | }); | |||
| 459 | + | ||||
| 460 | + | test('the tree takes no arrow key while the finder shows its results, nor any other key', () => { | |||
| 461 | + | const body = table('a', 'b'); | |||
| 462 | + | body.removeAttribute('data-rows'); | |||
| 463 | + | assert.equal(T.arrow('ArrowDown'), false); | |||
| 464 | + | assert.equal(cursorName(body), 'a'); | |||
| 465 | + | body.setAttribute('data-rows', ''); | |||
| 466 | + | assert.equal(T.arrow('x'), false); | |||
| 467 | + | assert.equal(T.arrow('PageDown'), false); | |||
| 468 | + | }); | |||
M internal/adapters/web/overlays.templ +4 -3
| @@ -13,10 +13,11 @@ templ OverlayHelp(mr bool) { | |||
| 13 | 13 | <div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div> | |
| 14 | 14 | <div><dt><kbd>?</kbd></dt><dd>this list</dd></div> | |
| 15 | 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> | |
| 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> | |
| 16 | + | <div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor; on Files, a directory opens or closes in place</dd></div> | |
| 17 | + | <div><dt><kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> <kbd>home</kbd> <kbd>end</kbd></dt><dd>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</dd></div> | |
| 18 | + | <div><dt><kbd>l</kbd> <kbd>h</kbd></dt><dd>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</dd></div> | |
| 18 | 19 | <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div> | |
| 19 | - | <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file in the repository</dd></div> | |
| 20 | + | <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div> | |
| 20 | 21 | <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div> | |
| 21 | 22 | <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd></dt><dd>tabs: summary, files, commits, branches & tags, merge requests</dd></div> | |
| 22 | 23 | <div><dt><kbd>g h</kbd></dt><dd>home</dd></div> | |
| @@ -13,10 +13,11 @@ templ OverlayHelp(mr bool) { | |||||
| 13 | <div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div> | 13 | <div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div> | ||
| 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; on Files, a directory opens or closes in place</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>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> <kbd>home</kbd> <kbd>end</kbd></dt><dd>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</dd></div> |
| 18 | + | <div><dt><kbd>l</kbd> <kbd>h</kbd></dt><dd>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</dd></div> | |||
| 18 | <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div> | 19 | <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div> | ||
| 19 | - | <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file in the repository</dd></div> | 20 | + | <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div> |
| 20 | <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div> | 21 | <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div> | ||
| 21 | <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd></dt><dd>tabs: summary, files, commits, branches & tags, merge requests</dd></div> | 22 | <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd></dt><dd>tabs: summary, files, commits, branches & tags, merge requests</dd></div> | ||
| 22 | <div><dt><kbd>g h</kbd></dt><dd>home</dd></div> | 23 | <div><dt><kbd>g h</kbd></dt><dd>home</dd></div> | ||
M internal/adapters/web/overlays_templ.go +10 -10
| @@ -35,14 +35,14 @@ func OverlayHelp(mr bool) templ.Component { | |||
| 35 | 35 | templ_7745c5c3_Var1 = templ.NopComponent | |
| 36 | 36 | } | |
| 37 | 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>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>") | |
| 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; on Files, a directory opens or closes in place</dd></div><div><dt><kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> <kbd>home</kbd> <kbd>end</kbd></dt><dd>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</dd></div><div><dt><kbd>l</kbd> <kbd>h</kbd></dt><dd>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</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 anywhere in the repository (esc clears, enter opens the best match)</dd></div><div><dt><kbd>b</kbd></dt><dd>") | |
| 39 | 39 | if templ_7745c5c3_Err != nil { | |
| 40 | 40 | return templ_7745c5c3_Err | |
| 41 | 41 | } | |
| 42 | 42 | var templ_7745c5c3_Var2 string | |
| 43 | 43 | templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("switch") | |
| 44 | 44 | if templ_7745c5c3_Err != nil { | |
| 45 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 20, Col: 43} | |
| 45 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 21, Col: 43} | |
| 46 | 46 | } | |
| 47 | 47 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) | |
| 48 | 48 | if templ_7745c5c3_Err != nil { | |
| @@ -96,7 +96,7 @@ func OverlayRefs(l Layout) templ.Component { | |||
| 96 | 96 | var templ_7745c5c3_Var4 string | |
| 97 | 97 | templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(l.Heads))) | |
| 98 | 98 | if templ_7745c5c3_Err != nil { | |
| 99 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 55, Col: 58} | |
| 99 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 56, Col: 58} | |
| 100 | 100 | } | |
| 101 | 101 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) | |
| 102 | 102 | if templ_7745c5c3_Err != nil { | |
| @@ -119,7 +119,7 @@ func OverlayRefs(l Layout) templ.Component { | |||
| 119 | 119 | var templ_7745c5c3_Var5 string | |
| 120 | 120 | templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(l.Tags))) | |
| 121 | 121 | if templ_7745c5c3_Err != nil { | |
| 122 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 63, Col: 53} | |
| 122 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 64, Col: 53} | |
| 123 | 123 | } | |
| 124 | 124 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) | |
| 125 | 125 | if templ_7745c5c3_Err != nil { | |
| @@ -172,7 +172,7 @@ func refItem(r RefItem) templ.Component { | |||
| 172 | 172 | var templ_7745c5c3_Var7 templ.SafeURL | |
| 173 | 173 | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | |
| 174 | 174 | if templ_7745c5c3_Err != nil { | |
| 175 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 78, Col: 31} | |
| 175 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 79, Col: 31} | |
| 176 | 176 | } | |
| 177 | 177 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) | |
| 178 | 178 | if templ_7745c5c3_Err != nil { | |
| @@ -210,7 +210,7 @@ func refItem(r RefItem) templ.Component { | |||
| 210 | 210 | var templ_7745c5c3_Var8 string | |
| 211 | 211 | templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 212 | 212 | if templ_7745c5c3_Err != nil { | |
| 213 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 89, Col: 18} | |
| 213 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 90, Col: 18} | |
| 214 | 214 | } | |
| 215 | 215 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) | |
| 216 | 216 | if templ_7745c5c3_Err != nil { | |
| @@ -259,7 +259,7 @@ func OverlayCmd(items []CmdItem) templ.Component { | |||
| 259 | 259 | var templ_7745c5c3_Var10 string | |
| 260 | 260 | templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(strings.ToLower(c.Kind + " " + c.Label)) | |
| 261 | 261 | if templ_7745c5c3_Err != nil { | |
| 262 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 114, Col: 56} | |
| 262 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 115, Col: 56} | |
| 263 | 263 | } | |
| 264 | 264 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) | |
| 265 | 265 | if templ_7745c5c3_Err != nil { | |
| @@ -282,7 +282,7 @@ func OverlayCmd(items []CmdItem) templ.Component { | |||
| 282 | 282 | var templ_7745c5c3_Var11 templ.SafeURL | |
| 283 | 283 | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | |
| 284 | 284 | if templ_7745c5c3_Err != nil { | |
| 285 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 114, Col: 114} | |
| 285 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 115, Col: 114} | |
| 286 | 286 | } | |
| 287 | 287 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | |
| 288 | 288 | if templ_7745c5c3_Err != nil { | |
| @@ -295,7 +295,7 @@ func OverlayCmd(items []CmdItem) templ.Component { | |||
| 295 | 295 | var templ_7745c5c3_Var12 string | |
| 296 | 296 | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | |
| 297 | 297 | if templ_7745c5c3_Err != nil { | |
| 298 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 114, Col: 158} | |
| 298 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 115, Col: 158} | |
| 299 | 299 | } | |
| 300 | 300 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) | |
| 301 | 301 | if templ_7745c5c3_Err != nil { | |
| @@ -308,7 +308,7 @@ func OverlayCmd(items []CmdItem) templ.Component { | |||
| 308 | 308 | var templ_7745c5c3_Var13 string | |
| 309 | 309 | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(c.Kind) | |
| 310 | 310 | if templ_7745c5c3_Err != nil { | |
| 311 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 114, Col: 191} | |
| 311 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 115, Col: 191} | |
| 312 | 312 | } | |
| 313 | 313 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | |
| 314 | 314 | if templ_7745c5c3_Err != nil { | |
| @@ -35,14 +35,14 @@ func OverlayHelp(mr bool) templ.Component { | |||||
| 35 | templ_7745c5c3_Var1 = templ.NopComponent | 35 | 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>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>") | 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; on Files, a directory opens or closes in place</dd></div><div><dt><kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> <kbd>home</kbd> <kbd>end</kbd></dt><dd>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</dd></div><div><dt><kbd>l</kbd> <kbd>h</kbd></dt><dd>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</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 anywhere in the repository (esc clears, enter opens the best match)</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_Err | 40 | return templ_7745c5c3_Err | ||
| 41 | } | 41 | } | ||
| 42 | var templ_7745c5c3_Var2 string | 42 | 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: 20, Col: 43} | 45 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 21, 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 string | 96 | 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: 55, Col: 58} | 99 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 56, 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 string | 119 | 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: 63, Col: 53} | 122 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 64, 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.SafeURL | 172 | 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: 78, Col: 31} | 175 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 79, 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 string | 210 | 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: 89, Col: 18} | 213 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 90, 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 string | 259 | 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: 114, Col: 56} | 262 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 115, 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.SafeURL | 282 | 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: 114, Col: 114} | 285 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 115, 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 string | 295 | 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: 114, Col: 158} | 298 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 115, 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 string | 308 | 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: 114, Col: 191} | 311 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 115, 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/css/30-list.css +95 -13
| @@ -70,17 +70,6 @@ | |||
| 70 | 70 | & .repo-meta time { color: var(--ink-dim); } | |
| 71 | 71 | & .repo-branch { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); color: var(--ink-dim); } | |
| 72 | 72 | } | |
| 73 | - | .list.tree { | |
| 74 | - | & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; } | |
| 75 | - | /* --depth is set by files.js on the rows opened in place */ | |
| 76 | - | & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); padding-inline-start: calc(var(--s3) + var(--depth, 0) * 1.25ch); } | |
| 77 | - | & tr[aria-busy=true] .c-kind { opacity: .4; } | |
| 78 | - | & .tree-more td { padding-inline-start: calc(var(--s3) + 36px + var(--depth, 0) * 1.25ch); color: var(--ink-dim); font-size: var(--fs-s); } | |
| 79 | - | & .c-size { width: 90px; text-align: right; } | |
| 80 | - | & .c-when { width: 100px; } | |
| 81 | - | & .c-sub { width: auto; } | |
| 82 | - | & .c-who { width: 130px; } | |
| 83 | - | } | |
| 84 | 73 | .list.passkeys { | |
| 85 | 74 | & .c-name { width: auto; font-weight: 600; } | |
| 86 | 75 | & .c-when { width: 120px; font-size: var(--fs-s); } | |
| @@ -125,8 +114,101 @@ | |||
| 125 | 114 | ||
| 126 | 115 | /* file finder: the results replace the tree while the field has a query */ | |
| 127 | 116 | #finder { | |
| 128 | - | margin-top: var(--s3); | |
| 129 | - | & #finder-status { margin-bottom: var(--s2); font-size: var(--fs-s); } | |
| 117 | + | & #finder-status { padding: var(--s3) var(--s4) var(--s2); font-size: var(--fs-s); } | |
| 118 | + | & #finder-list { padding: 0 var(--s2) var(--s2); } | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | /* the Files view: the finder on top, the tree in a card under it */ | |
| 122 | + | .tree-find { | |
| 123 | + | display: flex; align-items: center; gap: var(--s4); margin-bottom: var(--s3); | |
| 124 | + | & .filter { flex: 1; max-width: none; } | |
| 125 | + | & .filter::before { top: 14px; left: 14px; } | |
| 126 | + | & .filter::after { right: 12px; } | |
| 127 | + | & .filter input { height: 44px; padding-left: 42px; font-size: var(--fs); background: var(--bg-surface); } | |
| 128 | + | } | |
| 129 | + | .tree-keys { flex: none; color: var(--ink-mute); font-size: var(--fs-xs); white-space: nowrap; } | |
| 130 | + | .tree-card { | |
| 131 | + | background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; | |
| 132 | + | } | |
| 133 | + | .tree-head { | |
| 134 | + | display: flex; align-items: center; gap: var(--s3); min-height: 40px; padding: 0 var(--s4); | |
| 135 | + | background: var(--bg-raised); border-bottom: 1px solid var(--border); font-size: var(--fs-s); | |
| 136 | + | & .tree-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 700 var(--fs-s) / 1 var(--mono); color: var(--ink-hi); } | |
| 137 | + | & .tree-up { flex: none; font: 600 var(--fs-s) / 1 var(--mono); color: var(--ink-dim); text-decoration: none; } | |
| 138 | + | & .tree-up:hover { color: var(--ink-hi); text-decoration: underline; } | |
| 139 | + | & .tree-count { flex: none; color: var(--ink-mute); } | |
| 140 | + | } | |
| 141 | + | .tree-card > :is(.lede, .note) { margin: var(--s3) var(--s4); } | |
| 142 | + | ||
| 143 | + | /* The tree: one flat list, a row per entry, each as deep as --depth says | |
| 144 | + | (files.js sets it on the rows it opens in place). A row draws, from the | |
| 145 | + | left: a vertical guide per level above it, the branch tick to its node, | |
| 146 | + | the node, its name; then size, last commit and date, which are secondary. */ | |
| 147 | + | ul.tree { | |
| 148 | + | --indent: 22px; --rowh: 32px; --guide: var(--border-strong); | |
| 149 | + | & > li { | |
| 150 | + | display: grid; grid-template-columns: minmax(0, 1fr) 72px minmax(0, 1.2fr) 96px; align-items: center; | |
| 151 | + | min-height: var(--rowh); padding-right: var(--s4); font-size: var(--fs-s); | |
| 152 | + | } | |
| 153 | + | & > li:hover, & > li[data-cursor] { background: var(--bg-raised); } | |
| 154 | + | & > li[data-cursor] { box-shadow: inset 3px 0 0 var(--ink-hi); } | |
| 155 | + | & > li:focus-visible { outline-offset: -2px; border-radius: 0; } | |
| 156 | + | & .t-main { display: flex; align-items: stretch; min-width: 0; min-height: var(--rowh); padding-left: var(--s3); } | |
| 157 | + | & .t-twig { | |
| 158 | + | flex: none; width: calc(var(--depth, 0) * var(--indent)); | |
| 159 | + | background: repeating-linear-gradient(90deg, | |
| 160 | + | transparent 0 calc(var(--indent) / 2), var(--guide) calc(var(--indent) / 2) calc(var(--indent) / 2 + 1px), | |
| 161 | + | transparent calc(var(--indent) / 2 + 1px) var(--indent)); | |
| 162 | + | } | |
| 163 | + | & .t-node { position: relative; flex: none; width: var(--indent); } | |
| 164 | + | & .t-node::before, & .t-node::after { content: ""; position: absolute; } | |
| 165 | + | /* the branch tick, from the guide of the directory to this node */ | |
| 166 | + | & .t-node::before { top: 50%; left: calc(var(--indent) / -2); right: calc(50% + 6px); height: 1px; background: var(--guide); } | |
| 167 | + | & > li[data-depth="0"] .t-node::before { display: none; } | |
| 168 | + | /* an open directory continues its guide down, to its first entry */ | |
| 169 | + | & > li[aria-expanded="true"] .t-node { | |
| 170 | + | background: linear-gradient(var(--guide), var(--guide)) center bottom / 1px calc(50% - 8px) no-repeat; | |
| 171 | + | } | |
| 172 | + | /* the node: a ring for a file, a chevron for a directory (it turns down | |
| 173 | + | once open), a diamond for a link, a square for a submodule */ | |
| 174 | + | & .t-node::after { | |
| 175 | + | top: 50%; left: 50%; width: 7px; height: 7px; margin: -4px 0 0 -4px; | |
| 176 | + | background: var(--bg-surface); border: 1px solid var(--ink-mute); border-radius: 50%; | |
| 177 | + | } | |
| 178 | + | & > li:hover .t-node::after, & > li[data-cursor] .t-node::after { background: var(--bg-raised); border-color: var(--ink-hi); } | |
| 179 | + | & > li[data-kind="dir"] .t-node::after { | |
| 180 | + | width: 16px; height: 16px; margin: -8px 0 0 -8px; border: 0; border-radius: 0; background: var(--ink-dim); | |
| 181 | + | mask: url(icons/chevron.svg) center / contain no-repeat; transform: rotate(-90deg); | |
| 182 | + | } | |
| 183 | + | & > li[data-kind="dir"][aria-expanded="true"] .t-node::after { transform: none; background: var(--ink-hi); } | |
| 184 | + | & > li[data-kind="link"] .t-node::after { border-radius: 1px; transform: rotate(45deg); } | |
| 185 | + | & > li[data-kind="module"] .t-node::after { border-radius: 1px; } | |
| 186 | + | & > li[aria-busy="true"] .t-node { opacity: .4; } | |
| 187 | + | & .t-name { | |
| 188 | + | align-self: center; min-width: 0; padding: 4px var(--s1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; | |
| 189 | + | font-family: var(--mono); font-size: var(--fs); color: var(--ink); text-decoration: none; cursor: pointer; | |
| 190 | + | } | |
| 191 | + | & > li[data-kind="dir"] .t-name { font-weight: 700; color: var(--ink-hi); } | |
| 192 | + | & > li[data-kind="link"] .t-name, & > li[data-kind="module"] .t-name { color: var(--ink-dim); cursor: default; } | |
| 193 | + | & > li:hover .t-name, & > li[data-cursor] .t-name { text-decoration: underline; } | |
| 194 | + | & .t-size { padding-right: var(--s4); text-align: right; font: var(--fs-xs) / 1 var(--mono); color: var(--ink-mute); font-variant-numeric: tabular-nums; } | |
| 195 | + | & .t-sub { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-dim); } | |
| 196 | + | & .t-sub a { color: inherit; text-decoration: none; } | |
| 197 | + | & .t-sub a:hover { color: var(--ink-hi); text-decoration: underline; } | |
| 198 | + | & .t-when { text-align: right; white-space: nowrap; color: var(--ink-mute); font-size: var(--fs-xs); } | |
| 199 | + | & > li.tree-more { grid-template-columns: minmax(0, 1fr); } | |
| 200 | + | & > li.tree-more .t-name { font-size: var(--fs-s); font-weight: 400; color: var(--ink-dim); text-decoration: underline; } | |
| 201 | + | & > li.tree-more .t-node::after { display: none; } | |
| 202 | + | } | |
| 203 | + | @media (max-width: 900px) { | |
| 204 | + | ul.tree > li { grid-template-columns: minmax(0, 1fr) 64px 80px; } | |
| 205 | + | ul.tree .t-sub { display: none; } | |
| 206 | + | } | |
| 207 | + | @media (max-width: 720px) { | |
| 208 | + | .tree-keys { display: none; } | |
| 209 | + | ul.tree { --indent: 18px; } | |
| 210 | + | ul.tree > li { grid-template-columns: minmax(0, 1fr) 72px; } | |
| 211 | + | ul.tree .t-size { display: none; } | |
| 130 | 212 | } | |
| 131 | 213 | ||
| 132 | 214 | /* section headings inside a page */ | |
| @@ -70,17 +70,6 @@ | |||||
| 70 | & .repo-meta time { color: var(--ink-dim); } | 70 | & .repo-meta time { color: var(--ink-dim); } | ||
| 71 | & .repo-branch { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); color: var(--ink-dim); } | 71 | & .repo-branch { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); color: var(--ink-dim); } | ||
| 72 | } | 72 | } | ||
| 73 | - | .list.tree { | |||
| 74 | - | & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; } | |||
| 75 | - | /* --depth is set by files.js on the rows opened in place */ | |||
| 76 | - | & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); padding-inline-start: calc(var(--s3) + var(--depth, 0) * 1.25ch); } | |||
| 77 | - | & tr[aria-busy=true] .c-kind { opacity: .4; } | |||
| 78 | - | & .tree-more td { padding-inline-start: calc(var(--s3) + 36px + var(--depth, 0) * 1.25ch); color: var(--ink-dim); font-size: var(--fs-s); } | |||
| 79 | - | & .c-size { width: 90px; text-align: right; } | |||
| 80 | - | & .c-when { width: 100px; } | |||
| 81 | - | & .c-sub { width: auto; } | |||
| 82 | - | & .c-who { width: 130px; } | |||
| 83 | - | } | |||
| 84 | .list.passkeys { | 73 | .list.passkeys { | ||
| 85 | & .c-name { width: auto; font-weight: 600; } | 74 | & .c-name { width: auto; font-weight: 600; } | ||
| 86 | & .c-when { width: 120px; font-size: var(--fs-s); } | 75 | & .c-when { width: 120px; font-size: var(--fs-s); } | ||
| @@ -125,8 +114,101 @@ | |||||
| 125 | 114 | ||||
| 126 | /* file finder: the results replace the tree while the field has a query */ | 115 | /* file finder: the results replace the tree while the field has a query */ | ||
| 127 | #finder { | 116 | #finder { | ||
| 128 | - | margin-top: var(--s3); | 117 | + | & #finder-status { padding: var(--s3) var(--s4) var(--s2); font-size: var(--fs-s); } |
| 129 | - | & #finder-status { margin-bottom: var(--s2); font-size: var(--fs-s); } | 118 | + | & #finder-list { padding: 0 var(--s2) var(--s2); } |
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | /* the Files view: the finder on top, the tree in a card under it */ | |||
| 122 | + | .tree-find { | |||
| 123 | + | display: flex; align-items: center; gap: var(--s4); margin-bottom: var(--s3); | |||
| 124 | + | & .filter { flex: 1; max-width: none; } | |||
| 125 | + | & .filter::before { top: 14px; left: 14px; } | |||
| 126 | + | & .filter::after { right: 12px; } | |||
| 127 | + | & .filter input { height: 44px; padding-left: 42px; font-size: var(--fs); background: var(--bg-surface); } | |||
| 128 | + | } | |||
| 129 | + | .tree-keys { flex: none; color: var(--ink-mute); font-size: var(--fs-xs); white-space: nowrap; } | |||
| 130 | + | .tree-card { | |||
| 131 | + | background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; | |||
| 132 | + | } | |||
| 133 | + | .tree-head { | |||
| 134 | + | display: flex; align-items: center; gap: var(--s3); min-height: 40px; padding: 0 var(--s4); | |||
| 135 | + | background: var(--bg-raised); border-bottom: 1px solid var(--border); font-size: var(--fs-s); | |||
| 136 | + | & .tree-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 700 var(--fs-s) / 1 var(--mono); color: var(--ink-hi); } | |||
| 137 | + | & .tree-up { flex: none; font: 600 var(--fs-s) / 1 var(--mono); color: var(--ink-dim); text-decoration: none; } | |||
| 138 | + | & .tree-up:hover { color: var(--ink-hi); text-decoration: underline; } | |||
| 139 | + | & .tree-count { flex: none; color: var(--ink-mute); } | |||
| 140 | + | } | |||
| 141 | + | .tree-card > :is(.lede, .note) { margin: var(--s3) var(--s4); } | |||
| 142 | + | ||||
| 143 | + | /* The tree: one flat list, a row per entry, each as deep as --depth says | |||
| 144 | + | (files.js sets it on the rows it opens in place). A row draws, from the | |||
| 145 | + | left: a vertical guide per level above it, the branch tick to its node, | |||
| 146 | + | the node, its name; then size, last commit and date, which are secondary. */ | |||
| 147 | + | ul.tree { | |||
| 148 | + | --indent: 22px; --rowh: 32px; --guide: var(--border-strong); | |||
| 149 | + | & > li { | |||
| 150 | + | display: grid; grid-template-columns: minmax(0, 1fr) 72px minmax(0, 1.2fr) 96px; align-items: center; | |||
| 151 | + | min-height: var(--rowh); padding-right: var(--s4); font-size: var(--fs-s); | |||
| 152 | + | } | |||
| 153 | + | & > li:hover, & > li[data-cursor] { background: var(--bg-raised); } | |||
| 154 | + | & > li[data-cursor] { box-shadow: inset 3px 0 0 var(--ink-hi); } | |||
| 155 | + | & > li:focus-visible { outline-offset: -2px; border-radius: 0; } | |||
| 156 | + | & .t-main { display: flex; align-items: stretch; min-width: 0; min-height: var(--rowh); padding-left: var(--s3); } | |||
| 157 | + | & .t-twig { | |||
| 158 | + | flex: none; width: calc(var(--depth, 0) * var(--indent)); | |||
| 159 | + | background: repeating-linear-gradient(90deg, | |||
| 160 | + | transparent 0 calc(var(--indent) / 2), var(--guide) calc(var(--indent) / 2) calc(var(--indent) / 2 + 1px), | |||
| 161 | + | transparent calc(var(--indent) / 2 + 1px) var(--indent)); | |||
| 162 | + | } | |||
| 163 | + | & .t-node { position: relative; flex: none; width: var(--indent); } | |||
| 164 | + | & .t-node::before, & .t-node::after { content: ""; position: absolute; } | |||
| 165 | + | /* the branch tick, from the guide of the directory to this node */ | |||
| 166 | + | & .t-node::before { top: 50%; left: calc(var(--indent) / -2); right: calc(50% + 6px); height: 1px; background: var(--guide); } | |||
| 167 | + | & > li[data-depth="0"] .t-node::before { display: none; } | |||
| 168 | + | /* an open directory continues its guide down, to its first entry */ | |||
| 169 | + | & > li[aria-expanded="true"] .t-node { | |||
| 170 | + | background: linear-gradient(var(--guide), var(--guide)) center bottom / 1px calc(50% - 8px) no-repeat; | |||
| 171 | + | } | |||
| 172 | + | /* the node: a ring for a file, a chevron for a directory (it turns down | |||
| 173 | + | once open), a diamond for a link, a square for a submodule */ | |||
| 174 | + | & .t-node::after { | |||
| 175 | + | top: 50%; left: 50%; width: 7px; height: 7px; margin: -4px 0 0 -4px; | |||
| 176 | + | background: var(--bg-surface); border: 1px solid var(--ink-mute); border-radius: 50%; | |||
| 177 | + | } | |||
| 178 | + | & > li:hover .t-node::after, & > li[data-cursor] .t-node::after { background: var(--bg-raised); border-color: var(--ink-hi); } | |||
| 179 | + | & > li[data-kind="dir"] .t-node::after { | |||
| 180 | + | width: 16px; height: 16px; margin: -8px 0 0 -8px; border: 0; border-radius: 0; background: var(--ink-dim); | |||
| 181 | + | mask: url(icons/chevron.svg) center / contain no-repeat; transform: rotate(-90deg); | |||
| 182 | + | } | |||
| 183 | + | & > li[data-kind="dir"][aria-expanded="true"] .t-node::after { transform: none; background: var(--ink-hi); } | |||
| 184 | + | & > li[data-kind="link"] .t-node::after { border-radius: 1px; transform: rotate(45deg); } | |||
| 185 | + | & > li[data-kind="module"] .t-node::after { border-radius: 1px; } | |||
| 186 | + | & > li[aria-busy="true"] .t-node { opacity: .4; } | |||
| 187 | + | & .t-name { | |||
| 188 | + | align-self: center; min-width: 0; padding: 4px var(--s1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; | |||
| 189 | + | font-family: var(--mono); font-size: var(--fs); color: var(--ink); text-decoration: none; cursor: pointer; | |||
| 190 | + | } | |||
| 191 | + | & > li[data-kind="dir"] .t-name { font-weight: 700; color: var(--ink-hi); } | |||
| 192 | + | & > li[data-kind="link"] .t-name, & > li[data-kind="module"] .t-name { color: var(--ink-dim); cursor: default; } | |||
| 193 | + | & > li:hover .t-name, & > li[data-cursor] .t-name { text-decoration: underline; } | |||
| 194 | + | & .t-size { padding-right: var(--s4); text-align: right; font: var(--fs-xs) / 1 var(--mono); color: var(--ink-mute); font-variant-numeric: tabular-nums; } | |||
| 195 | + | & .t-sub { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-dim); } | |||
| 196 | + | & .t-sub a { color: inherit; text-decoration: none; } | |||
| 197 | + | & .t-sub a:hover { color: var(--ink-hi); text-decoration: underline; } | |||
| 198 | + | & .t-when { text-align: right; white-space: nowrap; color: var(--ink-mute); font-size: var(--fs-xs); } | |||
| 199 | + | & > li.tree-more { grid-template-columns: minmax(0, 1fr); } | |||
| 200 | + | & > li.tree-more .t-name { font-size: var(--fs-s); font-weight: 400; color: var(--ink-dim); text-decoration: underline; } | |||
| 201 | + | & > li.tree-more .t-node::after { display: none; } | |||
| 202 | + | } | |||
| 203 | + | @media (max-width: 900px) { | |||
| 204 | + | ul.tree > li { grid-template-columns: minmax(0, 1fr) 64px 80px; } | |||
| 205 | + | ul.tree .t-sub { display: none; } | |||
| 206 | + | } | |||
| 207 | + | @media (max-width: 720px) { | |||
| 208 | + | .tree-keys { display: none; } | |||
| 209 | + | ul.tree { --indent: 18px; } | |||
| 210 | + | ul.tree > li { grid-template-columns: minmax(0, 1fr) 72px; } | |||
| 211 | + | ul.tree .t-size { display: none; } | |||
| 130 | } | 212 | } | ||
| 131 | 213 | ||||
| 132 | /* section headings inside a page */ | 214 | /* section headings inside a page */ | ||
M internal/adapters/web/static/css/50-code.css +1 -1
| @@ -170,7 +170,7 @@ | |||
| 170 | 170 | .rail section > p { overflow-wrap: anywhere; } | |
| 171 | 171 | @media (max-width: 700px) { | |
| 172 | 172 | /* the name takes what the fixed columns leave */ | |
| 173 | - | .list.tree .c-name, .list.refs .c-name { width: auto; } | |
| 173 | + | .list.refs .c-name { width: auto; } | |
| 174 | 174 | } | |
| 175 | 175 | } | |
| 176 | 176 | ||
| @@ -170,7 +170,7 @@ | |||||
| 170 | .rail section > p { overflow-wrap: anywhere; } | 170 | .rail section > p { overflow-wrap: anywhere; } | ||
| 171 | @media (max-width: 700px) { | 171 | @media (max-width: 700px) { | ||
| 172 | /* the name takes what the fixed columns leave */ | 172 | /* the name takes what the fixed columns leave */ | ||
| 173 | - | .list.tree .c-name, .list.refs .c-name { width: auto; } | 173 | + | .list.refs .c-name { width: auto; } |
| 174 | } | 174 | } | ||
| 175 | } | 175 | } | ||
| 176 | 176 | ||||
M internal/adapters/web/static/js/ermite.js +13 -4
| @@ -62,6 +62,7 @@ | |||
| 62 | 62 | const j = Math.max(0, Math.min(rows.length - 1, i + d)); | |
| 63 | 63 | setCursor(rows[j], $$('[data-rows] > [data-cursor]')); | |
| 64 | 64 | rows[j].scrollIntoView({ block: 'nearest' }); | |
| 65 | + | window.ermiteTree?.sync(rows[j]); | |
| 65 | 66 | const mark = j + 1 + '/' + rows.length; | |
| 66 | 67 | body().dataset.nav = mark; | |
| 67 | 68 | setTimeout(() => body().dataset.nav === mark && (body().dataset.nav = ''), 1500); | |
| @@ -73,15 +74,19 @@ | |||
| 73 | 74 | if (!row) return scrollPage(last ? 1e9 : 0); | |
| 74 | 75 | setCursor(row, $$('[data-rows] > [data-cursor]')); | |
| 75 | 76 | row.scrollIntoView({ block: 'nearest' }); | |
| 77 | + | window.ermiteTree?.sync(row); | |
| 76 | 78 | }, | |
| 77 | 79 | open() { | |
| 78 | - | if (!$('dialog[open]')) $('[data-cursor] a')?.click(); | |
| 80 | + | if ($('dialog[open]') || window.ermiteTree?.open()) return; | |
| 81 | + | $('[data-cursor] a')?.click(); | |
| 79 | 82 | }, | |
| 80 | - | // l / h: into the side panel (the current file of a merge request, else | |
| 81 | - | // its main action, else its first control) and back to the content. | |
| 82 | - | // Enter then activates the control. | |
| 83 | + | // l / h: in the Files view, open a directory or a file / close a directory | |
| 84 | + | // or go up. Elsewhere: into the side panel (the current file of a merge | |
| 85 | + | // request, else its main action, else its first control) and back to the | |
| 86 | + | // content. Enter then activates the control. | |
| 83 | 87 | rail(into) { | |
| 84 | 88 | if ($('dialog[open]')) return; | |
| 89 | + | if (window.ermiteTree?.key(into)) return; | |
| 85 | 90 | if (into && window.ermiteMR?.focusFiles()) return flash('files · j k move · enter open · h back'); | |
| 86 | 91 | if (!into && window.ermiteMR?.leaveFiles()) return; | |
| 87 | 92 | const msg = into ? window.ermiteRail.enter() : window.ermiteRail.leave(); | |
| @@ -425,6 +430,10 @@ | |||
| 425 | 430 | return; | |
| 426 | 431 | } | |
| 427 | 432 | if (e.key === 'g') { pending = 1; clearTimeout(pendingTimer); pendingTimer = setTimeout(() => (pending = 0), 1200); return; } | |
| 433 | + | // The arrows and Home / End are the tree's only while the focus is on one of | |
| 434 | + | // its rows (they scroll, or move the focus, anywhere else); j k l h are | |
| 435 | + | // page-level keys, as on the other lists. | |
| 436 | + | if (!e.shiftKey && e.target.closest?.('#tree-rows') && window.ermiteTree?.arrow(e.key)) { e.preventDefault(); return; } | |
| 428 | 437 | const name = single[e.key]; | |
| 429 | 438 | if (!name) return; | |
| 430 | 439 | if (e.key !== 'Enter' || !$('dialog[open]')) e.preventDefault(); | |
| @@ -62,6 +62,7 @@ | |||||
| 62 | const j = Math.max(0, Math.min(rows.length - 1, i + d)); | 62 | const j = Math.max(0, Math.min(rows.length - 1, i + d)); | ||
| 63 | setCursor(rows[j], $$('[data-rows] > [data-cursor]')); | 63 | setCursor(rows[j], $$('[data-rows] > [data-cursor]')); | ||
| 64 | rows[j].scrollIntoView({ block: 'nearest' }); | 64 | rows[j].scrollIntoView({ block: 'nearest' }); | ||
| 65 | + | window.ermiteTree?.sync(rows[j]); | |||
| 65 | const mark = j + 1 + '/' + rows.length; | 66 | const mark = j + 1 + '/' + rows.length; | ||
| 66 | body().dataset.nav = mark; | 67 | body().dataset.nav = mark; | ||
| 67 | setTimeout(() => body().dataset.nav === mark && (body().dataset.nav = ''), 1500); | 68 | setTimeout(() => body().dataset.nav === mark && (body().dataset.nav = ''), 1500); | ||
| @@ -73,15 +74,19 @@ | |||||
| 73 | if (!row) return scrollPage(last ? 1e9 : 0); | 74 | if (!row) return scrollPage(last ? 1e9 : 0); | ||
| 74 | setCursor(row, $$('[data-rows] > [data-cursor]')); | 75 | setCursor(row, $$('[data-rows] > [data-cursor]')); | ||
| 75 | row.scrollIntoView({ block: 'nearest' }); | 76 | row.scrollIntoView({ block: 'nearest' }); | ||
| 77 | + | window.ermiteTree?.sync(row); | |||
| 76 | }, | 78 | }, | ||
| 77 | open() { | 79 | open() { | ||
| 78 | - | if (!$('dialog[open]')) $('[data-cursor] a')?.click(); | 80 | + | if ($('dialog[open]') || window.ermiteTree?.open()) return; |
| 81 | + | $('[data-cursor] a')?.click(); | |||
| 79 | }, | 82 | }, | ||
| 80 | - | // l / h: into the side panel (the current file of a merge request, else | 83 | + | // l / h: in the Files view, open a directory or a file / close a directory |
| 81 | - | // its main action, else its first control) and back to the content. | 84 | + | // or go up. Elsewhere: into the side panel (the current file of a merge |
| 82 | - | // Enter then activates the control. | 85 | + | // request, else its main action, else its first control) and back to the |
| 86 | + | // content. Enter then activates the control. | |||
| 83 | rail(into) { | 87 | rail(into) { | ||
| 84 | if ($('dialog[open]')) return; | 88 | if ($('dialog[open]')) return; | ||
| 89 | + | if (window.ermiteTree?.key(into)) return; | |||
| 85 | if (into && window.ermiteMR?.focusFiles()) return flash('files · j k move · enter open · h back'); | 90 | if (into && window.ermiteMR?.focusFiles()) return flash('files · j k move · enter open · h back'); | ||
| 86 | if (!into && window.ermiteMR?.leaveFiles()) return; | 91 | if (!into && window.ermiteMR?.leaveFiles()) return; | ||
| 87 | const msg = into ? window.ermiteRail.enter() : window.ermiteRail.leave(); | 92 | const msg = into ? window.ermiteRail.enter() : window.ermiteRail.leave(); | ||
| @@ -425,6 +430,10 @@ | |||||
| 425 | return; | 430 | return; | ||
| 426 | } | 431 | } | ||
| 427 | if (e.key === 'g') { pending = 1; clearTimeout(pendingTimer); pendingTimer = setTimeout(() => (pending = 0), 1200); return; } | 432 | if (e.key === 'g') { pending = 1; clearTimeout(pendingTimer); pendingTimer = setTimeout(() => (pending = 0), 1200); return; } | ||
| 433 | + | // The arrows and Home / End are the tree's only while the focus is on one of | |||
| 434 | + | // its rows (they scroll, or move the focus, anywhere else); j k l h are | |||
| 435 | + | // page-level keys, as on the other lists. | |||
| 436 | + | if (!e.shiftKey && e.target.closest?.('#tree-rows') && window.ermiteTree?.arrow(e.key)) { e.preventDefault(); return; } | |||
| 428 | const name = single[e.key]; | 437 | const name = single[e.key]; | ||
| 429 | if (!name) return; | 438 | if (!name) return; | ||
| 430 | if (e.key !== 'Enter' || !$('dialog[open]')) e.preventDefault(); | 439 | if (e.key !== 'Enter' || !$('dialog[open]')) e.preventDefault(); | ||
M internal/adapters/web/static/js/files.js +224 -59
| @@ -1,99 +1,264 @@ | |||
| 1 | - | // files.js: the Files view. A directory opens in place: its entries are | |
| 2 | - | // fetched once as table rows (the tree page with ?rows), inserted under it | |
| 3 | - | // and kept in the DOM, so closing and opening it again needs no request. | |
| 1 | + | // files.js: the Files view. The entries of a directory are the rows of a tree | |
| 2 | + | // (li, flat in one ul, each with its depth). A directory opens in place: its | |
| 3 | + | // entries are fetched once as rows (the tree page with ?rows), inserted under | |
| 4 | + | // it and kept in the DOM, so closing and opening it again needs no request. | |
| 4 | 5 | // | |
| 5 | - | // Rows opened in place carry data-depth and the --depth custom property | |
| 6 | - | // (set through the CSSOM: the CSP forbids style attributes). Rows of the | |
| 7 | - | // page itself have depth 0. Nothing here touches the document at load; the | |
| 8 | - | // handlers are declared in the templates (hx-on:click on #tree-rows). | |
| 6 | + | // Rows opened in place carry data-depth, aria-level and the --depth custom | |
| 7 | + | // property (set through the CSSOM: the CSP forbids style attributes), which | |
| 8 | + | // the stylesheet turns into indentation guides. Rows of the page itself have | |
| 9 | + | // depth 0. | |
| 10 | + | // | |
| 11 | + | // Focus model: a roving tabindex on the rows. The row under the cursor | |
| 12 | + | // (data-cursor) is the only one in the tab order (tabindex 0) and it has the | |
| 13 | + | // DOM focus whenever the keyboard or a click moved the cursor, so j k l h, the | |
| 14 | + | // arrows, Enter and Tab all act on the row that is highlighted. The name link | |
| 15 | + | // of a row is out of the tab order (the row stands for it: Enter opens it); | |
| 16 | + | // the link to the last commit is tabbable only in the row under the cursor | |
| 17 | + | // (Tab from the row reaches it, the next Tab leaves the tree). Without | |
| 18 | + | // scripts nothing is taken out of the tab order. The handlers are declared in | |
| 19 | + | // the templates (hx-on:click on #tree-rows); init() runs once at load (a page | |
| 20 | + | // without a tree: nothing); ermite.js calls ermiteTree.key and open for h, l | |
| 21 | + | // and Enter, and ermiteTree.sync for the cursor moves of j k gg G. | |
| 9 | 22 | window.ermiteTree = (() => { | |
| 10 | - | const depthOf = (tr) => Number(tr.dataset.depth || 0); | |
| 11 | - | const linkOf = (tr) => tr.querySelector('a[data-expand]'); | |
| 12 | - | const isOpen = (a) => a.getAttribute('aria-expanded') === 'true'; | |
| 23 | + | const depthOf = (li) => Number(li.dataset.depth || 0); | |
| 24 | + | const isDir = (li) => li.hasAttribute('data-expand'); | |
| 25 | + | const isOpen = (li) => li.getAttribute('aria-expanded') === 'true'; | |
| 13 | 26 | // A directory is loaded when rows deeper than it follow it. | |
| 14 | - | const isLoaded = (tr) => { const n = tr.nextElementSibling; return !!n && depthOf(n) > depthOf(tr); }; | |
| 27 | + | const isLoaded = (li) => { const n = li.nextElementSibling; return !!n && depthOf(n) > depthOf(li); }; | |
| 28 | + | const nameLink = (li) => li.querySelector('a.t-name'); | |
| 29 | + | ||
| 30 | + | // tab puts a row in or out of the tab order, with its link to the last commit. | |
| 31 | + | function tab(li, on) { | |
| 32 | + | li.setAttribute('tabindex', on ? '0' : '-1'); | |
| 33 | + | for (const a of li.querySelectorAll('a.t-commit')) a.setAttribute('tabindex', on ? '0' : '-1'); | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // prep takes a row out of the tab order: the one under the cursor comes back. | |
| 37 | + | function prep(li) { | |
| 38 | + | tab(li, false); | |
| 39 | + | nameLink(li)?.setAttribute('tabindex', '-1'); | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // mark makes li the row under the cursor, the one stop of the tab order, | |
| 43 | + | // and with focus gives it the DOM focus (without scrolling: the caller does). | |
| 44 | + | function mark(li, focus) { | |
| 45 | + | for (const r of Array.from(li.parentElement.children)) { | |
| 46 | + | if (r === li || !(r.hasAttribute('data-cursor') || r.getAttribute('tabindex') === '0')) continue; | |
| 47 | + | r.removeAttribute('data-cursor'); | |
| 48 | + | tab(r, false); | |
| 49 | + | } | |
| 50 | + | li.setAttribute('data-cursor', ''); | |
| 51 | + | tab(li, true); | |
| 52 | + | if (focus) li.focus({ preventScroll: true }); | |
| 53 | + | } | |
| 15 | 54 | ||
| 16 | - | function setOpen(tr, a, open) { | |
| 17 | - | a.setAttribute('aria-expanded', open ? 'true' : 'false'); | |
| 18 | - | const kind = tr.querySelector('.c-kind'); | |
| 19 | - | if (kind) kind.textContent = open ? '▾' : '▸'; | |
| 55 | + | // setCursor moves the cursor, and so the focus, to li, and shows it. | |
| 56 | + | function setCursor(li) { | |
| 57 | + | mark(li, true); | |
| 58 | + | li.scrollIntoView({ block: 'nearest' }); | |
| 20 | 59 | } | |
| 21 | 60 | ||
| 22 | - | // collapse hides every row under tr. The cursor of a row it hides moves to | |
| 23 | - | // tr, so it never rests on a row that cannot be seen. | |
| 24 | - | function collapse(tr, a) { | |
| 25 | - | setOpen(tr, a, false); | |
| 26 | - | const d = depthOf(tr); | |
| 27 | - | for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) { | |
| 61 | + | // collapse hides every row under li. The cursor, and the focus, of a row it | |
| 62 | + | // hides move to li, so they never rest on a row that cannot be seen. | |
| 63 | + | function collapse(li) { | |
| 64 | + | li.setAttribute('aria-expanded', 'false'); | |
| 65 | + | const d = depthOf(li); | |
| 66 | + | let lostCursor = false; | |
| 67 | + | let lostFocus = false; | |
| 68 | + | for (let r = li.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) { | |
| 28 | 69 | r.hidden = true; | |
| 29 | - | if (r.hasAttribute('data-cursor')) { | |
| 30 | - | r.removeAttribute('data-cursor'); | |
| 31 | - | tr.setAttribute('data-cursor', ''); | |
| 32 | - | } | |
| 70 | + | if (r.hasAttribute('data-cursor')) lostCursor = true; | |
| 71 | + | if (r.contains(document.activeElement)) lostFocus = true; | |
| 33 | 72 | } | |
| 73 | + | if (lostCursor || lostFocus) mark(li, lostFocus); | |
| 34 | 74 | } | |
| 35 | 75 | ||
| 36 | - | // expand shows the rows under tr that no collapsed directory between hides. | |
| 37 | - | function expand(tr, a) { | |
| 38 | - | setOpen(tr, a, true); | |
| 39 | - | const d = depthOf(tr); | |
| 76 | + | // expand shows the rows under li that no collapsed directory between hides. | |
| 77 | + | function expand(li) { | |
| 78 | + | li.setAttribute('aria-expanded', 'true'); | |
| 79 | + | const d = depthOf(li); | |
| 40 | 80 | let closedAt = Infinity; // depth of the collapsed directory whose rows are skipped | |
| 41 | - | for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) { | |
| 81 | + | for (let r = li.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) { | |
| 42 | 82 | const rd = depthOf(r); | |
| 43 | 83 | if (rd <= closedAt) closedAt = Infinity; | |
| 44 | 84 | r.hidden = rd > closedAt; | |
| 45 | - | const la = linkOf(r); | |
| 46 | - | if (closedAt === Infinity && la && !isOpen(la)) closedAt = rd; | |
| 85 | + | if (closedAt === Infinity && isDir(r) && !isOpen(r)) closedAt = rd; | |
| 47 | 86 | } | |
| 48 | 87 | } | |
| 49 | 88 | ||
| 50 | - | async function load(tr, a) { | |
| 51 | - | tr.setAttribute('aria-busy', 'true'); | |
| 89 | + | async function load(li) { | |
| 90 | + | li.setAttribute('aria-busy', 'true'); | |
| 52 | 91 | try { | |
| 53 | - | const res = await fetch(a.dataset.expand, { headers: { Accept: 'text/html' } }); | |
| 92 | + | const res = await fetch(li.dataset.expand, { headers: { Accept: 'text/html' } }); | |
| 54 | 93 | if (!res.ok) throw new Error('HTTP ' + res.status); | |
| 55 | 94 | const t = document.createElement('template'); | |
| 56 | 95 | t.innerHTML = await res.text(); | |
| 57 | - | const d = depthOf(tr) + 1; | |
| 58 | - | const rows = Array.from(t.content.children).filter((r) => r.tagName === 'TR'); | |
| 96 | + | const d = depthOf(li) + 1; | |
| 97 | + | const rows = Array.from(t.content.children).filter((r) => r.tagName === 'LI'); | |
| 59 | 98 | if (!rows.length) throw new Error('no rows'); | |
| 60 | - | // An ancestor closed while the request was out: tr is hidden, and its | |
| 99 | + | // An ancestor closed while the request was out: li is hidden, and its | |
| 61 | 100 | // rows come in hidden, for the expand of that ancestor to show. | |
| 62 | - | const covered = tr.hidden; | |
| 101 | + | const covered = li.hidden; | |
| 63 | 102 | for (const r of rows) { | |
| 64 | 103 | r.dataset.depth = String(d); | |
| 104 | + | r.setAttribute('aria-level', String(d + 1)); | |
| 65 | 105 | r.style.setProperty('--depth', String(d)); | |
| 106 | + | prep(r); | |
| 66 | 107 | if (covered) r.hidden = true; | |
| 67 | 108 | } | |
| 68 | - | tr.after(...rows); | |
| 69 | - | tr.removeAttribute('aria-busy'); | |
| 70 | - | if (covered) setOpen(tr, a, true); | |
| 71 | - | else expand(tr, a); | |
| 109 | + | li.after(...rows); | |
| 110 | + | li.removeAttribute('aria-busy'); | |
| 111 | + | if (covered) li.setAttribute('aria-expanded', 'true'); | |
| 112 | + | else expand(li); | |
| 72 | 113 | } catch (e) { | |
| 73 | - | location.href = a.href; | |
| 114 | + | location.href = nameLink(li).href; | |
| 74 | 115 | } | |
| 75 | 116 | } | |
| 76 | 117 | ||
| 77 | - | function toggle(tr) { | |
| 78 | - | const a = linkOf(tr); | |
| 79 | - | if (!a || tr.getAttribute('aria-busy') === 'true') return; | |
| 80 | - | if (isOpen(a)) collapse(tr, a); | |
| 81 | - | else if (isLoaded(tr)) expand(tr, a); | |
| 82 | - | else load(tr, a); | |
| 118 | + | function toggle(li) { | |
| 119 | + | if (!isDir(li) || li.getAttribute('aria-busy') === 'true') return; | |
| 120 | + | if (isOpen(li)) collapse(li); | |
| 121 | + | else if (isLoaded(li)) expand(li); | |
| 122 | + | else load(li); | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | const rowsEl = () => document.getElementById('tree-rows'); | |
| 126 | + | const cursorRow = () => rowsEl()?.querySelector(':scope > [data-cursor]') ?? null; | |
| 127 | + | const flash = (m) => window.ermite?.flash?.(m); | |
| 128 | + | ||
| 129 | + | // parentOf is the visible row of the directory that holds li: the closest | |
| 130 | + | // row above it that is less deep. | |
| 131 | + | function parentOf(li) { | |
| 132 | + | const d = depthOf(li); | |
| 133 | + | for (let r = li.previousElementSibling; r; r = r.previousElementSibling) if (depthOf(r) < d) return r; | |
| 134 | + | return null; | |
| 83 | 135 | } | |
| 84 | 136 | ||
| 85 | 137 | return { | |
| 86 | - | // click opens or closes the directory under a plain left click; any | |
| 87 | - | // modified or other click keeps the link's own navigation. | |
| 138 | + | // click puts the cursor and the focus on the row under any click, and | |
| 139 | + | // opens or closes the directory under a plain left click on its name or its | |
| 140 | + | // node; any modified or other click, and a click on the last commit, keep | |
| 141 | + | // their own navigation. | |
| 88 | 142 | click(event) { | |
| 89 | - | const a = event.target.closest('a[data-expand]'); | |
| 90 | - | if (!a || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; | |
| 143 | + | const li = event.target.closest('li'); | |
| 144 | + | if (!li || !li.parentElement) return; | |
| 145 | + | mark(li, true); | |
| 146 | + | if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; | |
| 147 | + | if (!event.target.closest('.t-main') || !isDir(li)) return; | |
| 91 | 148 | event.preventDefault(); | |
| 92 | - | toggle(a.closest('tr')); | |
| 149 | + | toggle(li); | |
| 150 | + | }, | |
| 151 | + | ||
| 152 | + | // init takes the rows out of the tab order but the one under the cursor | |
| 153 | + | // (the first when none has it). It runs once the page has loaded, and | |
| 154 | + | // not at all without a tree. | |
| 155 | + | init() { | |
| 156 | + | const rows = typeof document === 'undefined' ? null : rowsEl(); | |
| 157 | + | if (!rows) return; | |
| 158 | + | const lis = Array.from(rows.children); | |
| 159 | + | for (const li of lis) prep(li); | |
| 160 | + | const cur = cursorRow() ?? lis[0]; | |
| 161 | + | if (cur) mark(cur, false); | |
| 162 | + | }, | |
| 163 | + | ||
| 164 | + | // sync follows a cursor that ermite.js moved (j k gg G): the focus goes | |
| 165 | + | // with it, and the tab stop. | |
| 166 | + | sync(li) { | |
| 167 | + | if (li.parentElement && li.parentElement.id === 'tree-rows') mark(li, true); | |
| 168 | + | }, | |
| 169 | + | ||
| 170 | + | // mark is for the finder, which takes the cursor away and gives it back. | |
| 171 | + | mark(li) { mark(li, false); }, | |
| 172 | + | ||
| 173 | + | // active says that h and l (and the arrows) walk the tree: it is shown, | |
| 174 | + | // not replaced by the results of the finder. | |
| 175 | + | active() { | |
| 176 | + | const rows = rowsEl(); | |
| 177 | + | return !!rows && rows.hasAttribute('data-rows'); | |
| 178 | + | }, | |
| 179 | + | ||
| 180 | + | // open is Enter on the row under the cursor: a directory opens or closes | |
| 181 | + | // in place, a file opens. It answers whether the tree took the key. | |
| 182 | + | open() { | |
| 183 | + | const li = this.active() && cursorRow(); | |
| 184 | + | if (!li) return false; | |
| 185 | + | if (isDir(li)) toggle(li); | |
| 186 | + | else if (nameLink(li)) nameLink(li).click(); | |
| 187 | + | else flash('a submodule has no page here'); | |
| 188 | + | return true; | |
| 189 | + | }, | |
| 190 | + | ||
| 191 | + | // arrow is a key of the tree pattern (WAI-ARIA APG Tree View) pressed with | |
| 192 | + | // the focus on a row: it only moves the focus or the expansion, and never | |
| 193 | + | // opens a file or leaves the page. It answers whether the key is one the | |
| 194 | + | // tree handles (the caller then keeps the browser from scrolling). | |
| 195 | + | // Down, Up: the next, the previous visible row. Home, End: the first, the last. | |
| 196 | + | // Right: a closed directory opens, an open one moves to its first entry, a | |
| 197 | + | // file does nothing. | |
| 198 | + | // Left: an open directory closes, any other row moves to its directory, | |
| 199 | + | // and a top-level row does nothing. | |
| 200 | + | arrow(key) { | |
| 201 | + | if (!this.active()) return false; | |
| 202 | + | const li = cursorRow(); | |
| 203 | + | if (!li) return false; | |
| 204 | + | const rows = Array.from(li.parentElement.children).filter((r) => !r.hidden); | |
| 205 | + | const i = rows.indexOf(li); | |
| 206 | + | switch (key) { | |
| 207 | + | case 'ArrowDown': if (rows[i + 1]) setCursor(rows[i + 1]); break; | |
| 208 | + | case 'ArrowUp': if (i > 0) setCursor(rows[i - 1]); break; | |
| 209 | + | case 'Home': if (rows[0]) setCursor(rows[0]); break; | |
| 210 | + | case 'End': if (rows.length) setCursor(rows[rows.length - 1]); break; | |
| 211 | + | case 'ArrowRight': | |
| 212 | + | if (!isDir(li)) break; | |
| 213 | + | if (!isOpen(li)) toggle(li); | |
| 214 | + | else if (rows[i + 1] && depthOf(rows[i + 1]) > depthOf(li)) setCursor(rows[i + 1]); | |
| 215 | + | break; | |
| 216 | + | case 'ArrowLeft': | |
| 217 | + | if (isDir(li) && isOpen(li)) collapse(li); | |
| 218 | + | else if (parentOf(li)) setCursor(parentOf(li)); | |
| 219 | + | break; | |
| 220 | + | default: return false; | |
| 221 | + | } | |
| 222 | + | return true; | |
| 223 | + | }, | |
| 224 | + | ||
| 225 | + | // key is l (into = true) or h on the row under the cursor, the Vim keys | |
| 226 | + | // with their richer behaviour; it answers whether the tree took the key. | |
| 227 | + | // l: a closed directory opens, an open one moves to its first entry, a | |
| 228 | + | // file is opened. | |
| 229 | + | // h: an open directory closes, any other row goes to its directory, and a | |
| 230 | + | // top-level row to the parent directory's page. | |
| 231 | + | key(into) { | |
| 232 | + | if (!this.active()) return false; | |
| 233 | + | const li = cursorRow(); | |
| 234 | + | if (!li) return false; | |
| 235 | + | if (into) { | |
| 236 | + | if (isDir(li)) { | |
| 237 | + | if (!isOpen(li)) return toggle(li), true; | |
| 238 | + | const n = li.nextElementSibling; | |
| 239 | + | if (n && !n.hidden && depthOf(n) > depthOf(li)) setCursor(n); | |
| 240 | + | return true; | |
| 241 | + | } | |
| 242 | + | const a = nameLink(li); | |
| 243 | + | if (a) a.click(); | |
| 244 | + | else flash('a submodule has no page here'); | |
| 245 | + | return true; | |
| 246 | + | } | |
| 247 | + | if (isDir(li) && isOpen(li)) { | |
| 248 | + | collapse(li); | |
| 249 | + | return true; | |
| 250 | + | } | |
| 251 | + | const up = parentOf(li); | |
| 252 | + | if (up) setCursor(up); | |
| 253 | + | else if (rowsEl().dataset.up) location.href = rowsEl().dataset.up; | |
| 254 | + | else flash('top of the tree'); | |
| 255 | + | return true; | |
| 93 | 256 | }, | |
| 94 | 257 | }; | |
| 95 | 258 | })(); | |
| 96 | 259 | ||
| 260 | + | window.ermiteTree.init(); | |
| 261 | + | ||
| 97 | 262 | // ermiteFinder: the fuzzy finder of the Files view and the file hits of the | |
| 98 | 263 | // ":" palette. The core (index, search, positions, visible) is pure and runs | |
| 99 | 264 | // in Node; the DOM glue below it runs only from the handlers declared in the | |
| @@ -522,9 +687,9 @@ window.ermiteFinder = (() => { | |||
| 522 | 687 | rows.setAttribute('data-rows', ''); | |
| 523 | 688 | const keep = treeCursor; | |
| 524 | 689 | treeCursor = null; | |
| 525 | - | if (!rows.querySelector(':scope > tr[data-cursor]')) { | |
| 526 | - | const row = keep && keep.parentElement === rows && !keep.hidden ? keep : rows.querySelector(':scope > tr:not([hidden])'); | |
| 527 | - | row?.setAttribute('data-cursor', ''); | |
| 690 | + | if (!rows.querySelector(':scope > [data-cursor]')) { | |
| 691 | + | const row = keep && keep.parentElement === rows && !keep.hidden ? keep : rows.querySelector(':scope > li:not([hidden])'); | |
| 692 | + | if (row) window.ermiteTree.mark(row); | |
| 528 | 693 | } | |
| 529 | 694 | } | |
| 530 | 695 | ||
| @@ -535,7 +700,7 @@ window.ermiteFinder = (() => { | |||
| 535 | 700 | el.setAttribute('aria-expanded', 'true'); | |
| 536 | 701 | const rows = $('tree-rows'); | |
| 537 | 702 | rows.removeAttribute('data-rows'); | |
| 538 | - | rows.querySelectorAll(':scope > tr[data-cursor]').forEach((r) => { | |
| 703 | + | rows.querySelectorAll(':scope > [data-cursor]').forEach((r) => { | |
| 539 | 704 | treeCursor = r; | |
| 540 | 705 | r.removeAttribute('data-cursor'); | |
| 541 | 706 | }); | |
| @@ -1,99 +1,264 @@ | |||||
| 1 | - | // files.js: the Files view. A directory opens in place: its entries are | 1 | + | // files.js: the Files view. The entries of a directory are the rows of a tree |
| 2 | - | // fetched once as table rows (the tree page with ?rows), inserted under it | 2 | + | // (li, flat in one ul, each with its depth). A directory opens in place: its |
| 3 | - | // and kept in the DOM, so closing and opening it again needs no request. | 3 | + | // entries are fetched once as rows (the tree page with ?rows), inserted under |
| 4 | + | // it and kept in the DOM, so closing and opening it again needs no request. | |||
| 4 | // | 5 | // | ||
| 5 | - | // Rows opened in place carry data-depth and the --depth custom property | 6 | + | // Rows opened in place carry data-depth, aria-level and the --depth custom |
| 6 | - | // (set through the CSSOM: the CSP forbids style attributes). Rows of the | 7 | + | // property (set through the CSSOM: the CSP forbids style attributes), which |
| 7 | - | // page itself have depth 0. Nothing here touches the document at load; the | 8 | + | // the stylesheet turns into indentation guides. Rows of the page itself have |
| 8 | - | // handlers are declared in the templates (hx-on:click on #tree-rows). | 9 | + | // depth 0. |
| 10 | + | // | |||
| 11 | + | // Focus model: a roving tabindex on the rows. The row under the cursor | |||
| 12 | + | // (data-cursor) is the only one in the tab order (tabindex 0) and it has the | |||
| 13 | + | // DOM focus whenever the keyboard or a click moved the cursor, so j k l h, the | |||
| 14 | + | // arrows, Enter and Tab all act on the row that is highlighted. The name link | |||
| 15 | + | // of a row is out of the tab order (the row stands for it: Enter opens it); | |||
| 16 | + | // the link to the last commit is tabbable only in the row under the cursor | |||
| 17 | + | // (Tab from the row reaches it, the next Tab leaves the tree). Without | |||
| 18 | + | // scripts nothing is taken out of the tab order. The handlers are declared in | |||
| 19 | + | // the templates (hx-on:click on #tree-rows); init() runs once at load (a page | |||
| 20 | + | // without a tree: nothing); ermite.js calls ermiteTree.key and open for h, l | |||
| 21 | + | // and Enter, and ermiteTree.sync for the cursor moves of j k gg G. | |||
| 9 | window.ermiteTree = (() => { | 22 | window.ermiteTree = (() => { | ||
| 10 | - | const depthOf = (tr) => Number(tr.dataset.depth || 0); | 23 | + | const depthOf = (li) => Number(li.dataset.depth || 0); |
| 11 | - | const linkOf = (tr) => tr.querySelector('a[data-expand]'); | 24 | + | const isDir = (li) => li.hasAttribute('data-expand'); |
| 12 | - | const isOpen = (a) => a.getAttribute('aria-expanded') === 'true'; | 25 | + | const isOpen = (li) => li.getAttribute('aria-expanded') === 'true'; |
| 13 | // A directory is loaded when rows deeper than it follow it. | 26 | // A directory is loaded when rows deeper than it follow it. | ||
| 14 | - | const isLoaded = (tr) => { const n = tr.nextElementSibling; return !!n && depthOf(n) > depthOf(tr); }; | 27 | + | const isLoaded = (li) => { const n = li.nextElementSibling; return !!n && depthOf(n) > depthOf(li); }; |
| 28 | + | const nameLink = (li) => li.querySelector('a.t-name'); | |||
| 29 | + | ||||
| 30 | + | // tab puts a row in or out of the tab order, with its link to the last commit. | |||
| 31 | + | function tab(li, on) { | |||
| 32 | + | li.setAttribute('tabindex', on ? '0' : '-1'); | |||
| 33 | + | for (const a of li.querySelectorAll('a.t-commit')) a.setAttribute('tabindex', on ? '0' : '-1'); | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // prep takes a row out of the tab order: the one under the cursor comes back. | |||
| 37 | + | function prep(li) { | |||
| 38 | + | tab(li, false); | |||
| 39 | + | nameLink(li)?.setAttribute('tabindex', '-1'); | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // mark makes li the row under the cursor, the one stop of the tab order, | |||
| 43 | + | // and with focus gives it the DOM focus (without scrolling: the caller does). | |||
| 44 | + | function mark(li, focus) { | |||
| 45 | + | for (const r of Array.from(li.parentElement.children)) { | |||
| 46 | + | if (r === li || !(r.hasAttribute('data-cursor') || r.getAttribute('tabindex') === '0')) continue; | |||
| 47 | + | r.removeAttribute('data-cursor'); | |||
| 48 | + | tab(r, false); | |||
| 49 | + | } | |||
| 50 | + | li.setAttribute('data-cursor', ''); | |||
| 51 | + | tab(li, true); | |||
| 52 | + | if (focus) li.focus({ preventScroll: true }); | |||
| 53 | + | } | |||
| 15 | 54 | ||||
| 16 | - | function setOpen(tr, a, open) { | 55 | + | // setCursor moves the cursor, and so the focus, to li, and shows it. |
| 17 | - | a.setAttribute('aria-expanded', open ? 'true' : 'false'); | 56 | + | function setCursor(li) { |
| 18 | - | const kind = tr.querySelector('.c-kind'); | 57 | + | mark(li, true); |
| 19 | - | if (kind) kind.textContent = open ? '▾' : '▸'; | 58 | + | li.scrollIntoView({ block: 'nearest' }); |
| 20 | } | 59 | } | ||
| 21 | 60 | ||||
| 22 | - | // collapse hides every row under tr. The cursor of a row it hides moves to | 61 | + | // collapse hides every row under li. The cursor, and the focus, of a row it |
| 23 | - | // tr, so it never rests on a row that cannot be seen. | 62 | + | // hides move to li, so they never rest on a row that cannot be seen. |
| 24 | - | function collapse(tr, a) { | 63 | + | function collapse(li) { |
| 25 | - | setOpen(tr, a, false); | 64 | + | li.setAttribute('aria-expanded', 'false'); |
| 26 | - | const d = depthOf(tr); | 65 | + | const d = depthOf(li); |
| 27 | - | for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) { | 66 | + | let lostCursor = false; |
| 67 | + | let lostFocus = false; | |||
| 68 | + | for (let r = li.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) { | |||
| 28 | r.hidden = true; | 69 | r.hidden = true; | ||
| 29 | - | if (r.hasAttribute('data-cursor')) { | 70 | + | if (r.hasAttribute('data-cursor')) lostCursor = true; |
| 30 | - | r.removeAttribute('data-cursor'); | 71 | + | if (r.contains(document.activeElement)) lostFocus = true; |
| 31 | - | tr.setAttribute('data-cursor', ''); | |||
| 32 | - | } | |||
| 33 | } | 72 | } | ||
| 73 | + | if (lostCursor || lostFocus) mark(li, lostFocus); | |||
| 34 | } | 74 | } | ||
| 35 | 75 | ||||
| 36 | - | // expand shows the rows under tr that no collapsed directory between hides. | 76 | + | // expand shows the rows under li that no collapsed directory between hides. |
| 37 | - | function expand(tr, a) { | 77 | + | function expand(li) { |
| 38 | - | setOpen(tr, a, true); | 78 | + | li.setAttribute('aria-expanded', 'true'); |
| 39 | - | const d = depthOf(tr); | 79 | + | const d = depthOf(li); |
| 40 | let closedAt = Infinity; // depth of the collapsed directory whose rows are skipped | 80 | let closedAt = Infinity; // depth of the collapsed directory whose rows are skipped | ||
| 41 | - | for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) { | 81 | + | for (let r = li.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) { |
| 42 | const rd = depthOf(r); | 82 | const rd = depthOf(r); | ||
| 43 | if (rd <= closedAt) closedAt = Infinity; | 83 | if (rd <= closedAt) closedAt = Infinity; | ||
| 44 | r.hidden = rd > closedAt; | 84 | r.hidden = rd > closedAt; | ||
| 45 | - | const la = linkOf(r); | 85 | + | if (closedAt === Infinity && isDir(r) && !isOpen(r)) closedAt = rd; |
| 46 | - | if (closedAt === Infinity && la && !isOpen(la)) closedAt = rd; | |||
| 47 | } | 86 | } | ||
| 48 | } | 87 | } | ||
| 49 | 88 | ||||
| 50 | - | async function load(tr, a) { | 89 | + | async function load(li) { |
| 51 | - | tr.setAttribute('aria-busy', 'true'); | 90 | + | li.setAttribute('aria-busy', 'true'); |
| 52 | try { | 91 | try { | ||
| 53 | - | const res = await fetch(a.dataset.expand, { headers: { Accept: 'text/html' } }); | 92 | + | const res = await fetch(li.dataset.expand, { headers: { Accept: 'text/html' } }); |
| 54 | if (!res.ok) throw new Error('HTTP ' + res.status); | 93 | if (!res.ok) throw new Error('HTTP ' + res.status); | ||
| 55 | const t = document.createElement('template'); | 94 | const t = document.createElement('template'); | ||
| 56 | t.innerHTML = await res.text(); | 95 | t.innerHTML = await res.text(); | ||
| 57 | - | const d = depthOf(tr) + 1; | 96 | + | const d = depthOf(li) + 1; |
| 58 | - | const rows = Array.from(t.content.children).filter((r) => r.tagName === 'TR'); | 97 | + | const rows = Array.from(t.content.children).filter((r) => r.tagName === 'LI'); |
| 59 | if (!rows.length) throw new Error('no rows'); | 98 | if (!rows.length) throw new Error('no rows'); | ||
| 60 | - | // An ancestor closed while the request was out: tr is hidden, and its | 99 | + | // An ancestor closed while the request was out: li is hidden, and its |
| 61 | // rows come in hidden, for the expand of that ancestor to show. | 100 | // rows come in hidden, for the expand of that ancestor to show. | ||
| 62 | - | const covered = tr.hidden; | 101 | + | const covered = li.hidden; |
| 63 | for (const r of rows) { | 102 | for (const r of rows) { | ||
| 64 | r.dataset.depth = String(d); | 103 | r.dataset.depth = String(d); | ||
| 104 | + | r.setAttribute('aria-level', String(d + 1)); | |||
| 65 | r.style.setProperty('--depth', String(d)); | 105 | r.style.setProperty('--depth', String(d)); | ||
| 106 | + | prep(r); | |||
| 66 | if (covered) r.hidden = true; | 107 | if (covered) r.hidden = true; | ||
| 67 | } | 108 | } | ||
| 68 | - | tr.after(...rows); | 109 | + | li.after(...rows); |
| 69 | - | tr.removeAttribute('aria-busy'); | 110 | + | li.removeAttribute('aria-busy'); |
| 70 | - | if (covered) setOpen(tr, a, true); | 111 | + | if (covered) li.setAttribute('aria-expanded', 'true'); |
| 71 | - | else expand(tr, a); | 112 | + | else expand(li); |
| 72 | } catch (e) { | 113 | } catch (e) { | ||
| 73 | - | location.href = a.href; | 114 | + | location.href = nameLink(li).href; |
| 74 | } | 115 | } | ||
| 75 | } | 116 | } | ||
| 76 | 117 | ||||
| 77 | - | function toggle(tr) { | 118 | + | function toggle(li) { |
| 78 | - | const a = linkOf(tr); | 119 | + | if (!isDir(li) || li.getAttribute('aria-busy') === 'true') return; |
| 79 | - | if (!a || tr.getAttribute('aria-busy') === 'true') return; | 120 | + | if (isOpen(li)) collapse(li); |
| 80 | - | if (isOpen(a)) collapse(tr, a); | 121 | + | else if (isLoaded(li)) expand(li); |
| 81 | - | else if (isLoaded(tr)) expand(tr, a); | 122 | + | else load(li); |
| 82 | - | else load(tr, a); | 123 | + | } |
| 124 | + | ||||
| 125 | + | const rowsEl = () => document.getElementById('tree-rows'); | |||
| 126 | + | const cursorRow = () => rowsEl()?.querySelector(':scope > [data-cursor]') ?? null; | |||
| 127 | + | const flash = (m) => window.ermite?.flash?.(m); | |||
| 128 | + | ||||
| 129 | + | // parentOf is the visible row of the directory that holds li: the closest | |||
| 130 | + | // row above it that is less deep. | |||
| 131 | + | function parentOf(li) { | |||
| 132 | + | const d = depthOf(li); | |||
| 133 | + | for (let r = li.previousElementSibling; r; r = r.previousElementSibling) if (depthOf(r) < d) return r; | |||
| 134 | + | return null; | |||
| 83 | } | 135 | } | ||
| 84 | 136 | ||||
| 85 | return { | 137 | return { | ||
| 86 | - | // click opens or closes the directory under a plain left click; any | 138 | + | // click puts the cursor and the focus on the row under any click, and |
| 87 | - | // modified or other click keeps the link's own navigation. | 139 | + | // opens or closes the directory under a plain left click on its name or its |
| 140 | + | // node; any modified or other click, and a click on the last commit, keep | |||
| 141 | + | // their own navigation. | |||
| 88 | click(event) { | 142 | click(event) { | ||
| 89 | - | const a = event.target.closest('a[data-expand]'); | 143 | + | const li = event.target.closest('li'); |
| 90 | - | if (!a || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; | 144 | + | if (!li || !li.parentElement) return; |
| 145 | + | mark(li, true); | |||
| 146 | + | if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; | |||
| 147 | + | if (!event.target.closest('.t-main') || !isDir(li)) return; | |||
| 91 | event.preventDefault(); | 148 | event.preventDefault(); | ||
| 92 | - | toggle(a.closest('tr')); | 149 | + | toggle(li); |
| 150 | + | }, | |||
| 151 | + | ||||
| 152 | + | // init takes the rows out of the tab order but the one under the cursor | |||
| 153 | + | // (the first when none has it). It runs once the page has loaded, and | |||
| 154 | + | // not at all without a tree. | |||
| 155 | + | init() { | |||
| 156 | + | const rows = typeof document === 'undefined' ? null : rowsEl(); | |||
| 157 | + | if (!rows) return; | |||
| 158 | + | const lis = Array.from(rows.children); | |||
| 159 | + | for (const li of lis) prep(li); | |||
| 160 | + | const cur = cursorRow() ?? lis[0]; | |||
| 161 | + | if (cur) mark(cur, false); | |||
| 162 | + | }, | |||
| 163 | + | ||||
| 164 | + | // sync follows a cursor that ermite.js moved (j k gg G): the focus goes | |||
| 165 | + | // with it, and the tab stop. | |||
| 166 | + | sync(li) { | |||
| 167 | + | if (li.parentElement && li.parentElement.id === 'tree-rows') mark(li, true); | |||
| 168 | + | }, | |||
| 169 | + | ||||
| 170 | + | // mark is for the finder, which takes the cursor away and gives it back. | |||
| 171 | + | mark(li) { mark(li, false); }, | |||
| 172 | + | ||||
| 173 | + | // active says that h and l (and the arrows) walk the tree: it is shown, | |||
| 174 | + | // not replaced by the results of the finder. | |||
| 175 | + | active() { | |||
| 176 | + | const rows = rowsEl(); | |||
| 177 | + | return !!rows && rows.hasAttribute('data-rows'); | |||
| 178 | + | }, | |||
| 179 | + | ||||
| 180 | + | // open is Enter on the row under the cursor: a directory opens or closes | |||
| 181 | + | // in place, a file opens. It answers whether the tree took the key. | |||
| 182 | + | open() { | |||
| 183 | + | const li = this.active() && cursorRow(); | |||
| 184 | + | if (!li) return false; | |||
| 185 | + | if (isDir(li)) toggle(li); | |||
| 186 | + | else if (nameLink(li)) nameLink(li).click(); | |||
| 187 | + | else flash('a submodule has no page here'); | |||
| 188 | + | return true; | |||
| 189 | + | }, | |||
| 190 | + | ||||
| 191 | + | // arrow is a key of the tree pattern (WAI-ARIA APG Tree View) pressed with | |||
| 192 | + | // the focus on a row: it only moves the focus or the expansion, and never | |||
| 193 | + | // opens a file or leaves the page. It answers whether the key is one the | |||
| 194 | + | // tree handles (the caller then keeps the browser from scrolling). | |||
| 195 | + | // Down, Up: the next, the previous visible row. Home, End: the first, the last. | |||
| 196 | + | // Right: a closed directory opens, an open one moves to its first entry, a | |||
| 197 | + | // file does nothing. | |||
| 198 | + | // Left: an open directory closes, any other row moves to its directory, | |||
| 199 | + | // and a top-level row does nothing. | |||
| 200 | + | arrow(key) { | |||
| 201 | + | if (!this.active()) return false; | |||
| 202 | + | const li = cursorRow(); | |||
| 203 | + | if (!li) return false; | |||
| 204 | + | const rows = Array.from(li.parentElement.children).filter((r) => !r.hidden); | |||
| 205 | + | const i = rows.indexOf(li); | |||
| 206 | + | switch (key) { | |||
| 207 | + | case 'ArrowDown': if (rows[i + 1]) setCursor(rows[i + 1]); break; | |||
| 208 | + | case 'ArrowUp': if (i > 0) setCursor(rows[i - 1]); break; | |||
| 209 | + | case 'Home': if (rows[0]) setCursor(rows[0]); break; | |||
| 210 | + | case 'End': if (rows.length) setCursor(rows[rows.length - 1]); break; | |||
| 211 | + | case 'ArrowRight': | |||
| 212 | + | if (!isDir(li)) break; | |||
| 213 | + | if (!isOpen(li)) toggle(li); | |||
| 214 | + | else if (rows[i + 1] && depthOf(rows[i + 1]) > depthOf(li)) setCursor(rows[i + 1]); | |||
| 215 | + | break; | |||
| 216 | + | case 'ArrowLeft': | |||
| 217 | + | if (isDir(li) && isOpen(li)) collapse(li); | |||
| 218 | + | else if (parentOf(li)) setCursor(parentOf(li)); | |||
| 219 | + | break; | |||
| 220 | + | default: return false; | |||
| 221 | + | } | |||
| 222 | + | return true; | |||
| 223 | + | }, | |||
| 224 | + | ||||
| 225 | + | // key is l (into = true) or h on the row under the cursor, the Vim keys | |||
| 226 | + | // with their richer behaviour; it answers whether the tree took the key. | |||
| 227 | + | // l: a closed directory opens, an open one moves to its first entry, a | |||
| 228 | + | // file is opened. | |||
| 229 | + | // h: an open directory closes, any other row goes to its directory, and a | |||
| 230 | + | // top-level row to the parent directory's page. | |||
| 231 | + | key(into) { | |||
| 232 | + | if (!this.active()) return false; | |||
| 233 | + | const li = cursorRow(); | |||
| 234 | + | if (!li) return false; | |||
| 235 | + | if (into) { | |||
| 236 | + | if (isDir(li)) { | |||
| 237 | + | if (!isOpen(li)) return toggle(li), true; | |||
| 238 | + | const n = li.nextElementSibling; | |||
| 239 | + | if (n && !n.hidden && depthOf(n) > depthOf(li)) setCursor(n); | |||
| 240 | + | return true; | |||
| 241 | + | } | |||
| 242 | + | const a = nameLink(li); | |||
| 243 | + | if (a) a.click(); | |||
| 244 | + | else flash('a submodule has no page here'); | |||
| 245 | + | return true; | |||
| 246 | + | } | |||
| 247 | + | if (isDir(li) && isOpen(li)) { | |||
| 248 | + | collapse(li); | |||
| 249 | + | return true; | |||
| 250 | + | } | |||
| 251 | + | const up = parentOf(li); | |||
| 252 | + | if (up) setCursor(up); | |||
| 253 | + | else if (rowsEl().dataset.up) location.href = rowsEl().dataset.up; | |||
| 254 | + | else flash('top of the tree'); | |||
| 255 | + | return true; | |||
| 93 | }, | 256 | }, | ||
| 94 | }; | 257 | }; | ||
| 95 | })(); | 258 | })(); | ||
| 96 | 259 | ||||
| 260 | + | window.ermiteTree.init(); | |||
| 261 | + | ||||
| 97 | // ermiteFinder: the fuzzy finder of the Files view and the file hits of the | 262 | // ermiteFinder: the fuzzy finder of the Files view and the file hits of the | ||
| 98 | // ":" palette. The core (index, search, positions, visible) is pure and runs | 263 | // ":" palette. The core (index, search, positions, visible) is pure and runs | ||
| 99 | // in Node; the DOM glue below it runs only from the handlers declared in the | 264 | // in Node; the DOM glue below it runs only from the handlers declared in the | ||
| @@ -522,9 +687,9 @@ window.ermiteFinder = (() => { | |||||
| 522 | rows.setAttribute('data-rows', ''); | 687 | rows.setAttribute('data-rows', ''); | ||
| 523 | const keep = treeCursor; | 688 | const keep = treeCursor; | ||
| 524 | treeCursor = null; | 689 | treeCursor = null; | ||
| 525 | - | if (!rows.querySelector(':scope > tr[data-cursor]')) { | 690 | + | if (!rows.querySelector(':scope > [data-cursor]')) { |
| 526 | - | const row = keep && keep.parentElement === rows && !keep.hidden ? keep : rows.querySelector(':scope > tr:not([hidden])'); | 691 | + | const row = keep && keep.parentElement === rows && !keep.hidden ? keep : rows.querySelector(':scope > li:not([hidden])'); |
| 527 | - | row?.setAttribute('data-cursor', ''); | 692 | + | if (row) window.ermiteTree.mark(row); |
| 528 | } | 693 | } | ||
| 529 | } | 694 | } | ||
| 530 | 695 | ||||
| @@ -535,7 +700,7 @@ window.ermiteFinder = (() => { | |||||
| 535 | el.setAttribute('aria-expanded', 'true'); | 700 | el.setAttribute('aria-expanded', 'true'); | ||
| 536 | const rows = $('tree-rows'); | 701 | const rows = $('tree-rows'); | ||
| 537 | rows.removeAttribute('data-rows'); | 702 | rows.removeAttribute('data-rows'); | ||
| 538 | - | rows.querySelectorAll(':scope > tr[data-cursor]').forEach((r) => { | 703 | + | rows.querySelectorAll(':scope > [data-cursor]').forEach((r) => { |
| 539 | treeCursor = r; | 704 | treeCursor = r; | ||
| 540 | r.removeAttribute('data-cursor'); | 705 | r.removeAttribute('data-cursor'); | ||
| 541 | }); | 706 | }); | ||
M internal/adapters/web/tree.go +60 -20
| @@ -5,20 +5,33 @@ package web | |||
| 5 | 5 | import ( | |
| 6 | 6 | "context" | |
| 7 | 7 | "net/http" | |
| 8 | + | "path" | |
| 8 | 9 | ||
| 9 | 10 | "github.com/a-h/templ" | |
| 10 | 11 | ||
| 11 | 12 | "bnema.dev/ermite/internal/domain" | |
| 12 | 13 | ) | |
| 13 | 14 | ||
| 15 | + | // Entry kinds of a TreeRow; the stylesheet draws the node of a row by its kind. | |
| 16 | + | const ( | |
| 17 | + | kindDir = "dir" | |
| 18 | + | kindFile = "file" | |
| 19 | + | kindLink = "link" | |
| 20 | + | kindModule = "module" | |
| 21 | + | ) | |
| 22 | + | ||
| 14 | 23 | // TreeRow is one entry of a directory. | |
| 15 | 24 | type TreeRow struct { | |
| 16 | - | Glyph string // ▸ directory, @ symbolic link, + submodule, · file | |
| 17 | - | Name string | |
| 18 | - | Href string // "" for a submodule | |
| 25 | + | Kind string // kindDir, kindFile, kindLink or kindModule | |
| 26 | + | Name string | |
| 27 | + | Href string // "" for a submodule | |
| 19 | 28 | // Expand is the URL of the rows of a directory (Href?rows); "" for any | |
| 20 | 29 | // other entry. | |
| 21 | - | Expand string | |
| 30 | + | Expand string | |
| 31 | + | // Hint says what a directory, a link or a submodule is, in words, for the | |
| 32 | + | // screen reader (the node drawn for it is aria-hidden) and the tooltip; "" | |
| 33 | + | // for a file. | |
| 34 | + | Hint string | |
| 22 | 35 | Size string | |
| 23 | 36 | Subject string | |
| 24 | 37 | CHref string | |
| @@ -26,21 +39,39 @@ type TreeRow struct { | |||
| 26 | 39 | When string | |
| 27 | 40 | WhenAt string | |
| 28 | 41 | Cursor bool | |
| 42 | + | // Pos and Set are the aria-posinset and aria-setsize of the row among the | |
| 43 | + | // entries of its directory; Set is -1 when the listing is cut, as the | |
| 44 | + | // directory has more entries than are listed. | |
| 45 | + | Pos, Set int | |
| 29 | 46 | } | |
| 30 | 47 | ||
| 31 | 48 | // TreePage is a directory. | |
| 32 | 49 | type TreePage struct { | |
| 33 | - | Rows []TreeRow | |
| 34 | - | Readme *Readme | |
| 50 | + | Rows []TreeRow | |
| 51 | + | // Path is the directory, "/" for the root. | |
| 52 | + | Path string | |
| 53 | + | // Up is the page of the parent directory, "" at the root: the h key of the | |
| 54 | + | // tree goes there from a top-level entry. | |
| 55 | + | Up string | |
| 35 | 56 | // Notes say what the listing leaves out. | |
| 36 | 57 | Notes []string | |
| 37 | 58 | // More, for the rows of a directory expanded in place, is the page of the | |
| 38 | 59 | // directory when it has more entries than are listed; "" otherwise. | |
| 39 | 60 | More string | |
| 61 | + | // MorePos is the position of the row that says there are more. | |
| 62 | + | MorePos int | |
| 40 | 63 | } | |
| 41 | 64 | ||
| 42 | - | // tree lists a directory of a ref, with the last commit of each entry, and | |
| 43 | - | // renders the README of the directory below. | |
| 65 | + | // Unit is the noun of the entry count in the head of the tree. | |
| 66 | + | func (p TreePage) Unit() string { | |
| 67 | + | if len(p.Rows) == 1 { | |
| 68 | + | return "entry" | |
| 69 | + | } | |
| 70 | + | return "entries" | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // tree lists a directory of a ref, with the last commit of each entry. With | |
| 74 | + | // ?rows it answers the entries alone, for a directory opened in place. | |
| 44 | 75 | // | |
| 45 | 76 | //hexcheck:ignore suspicious-business-logic-in-adapter builds the directory page from the use case results; no rule is decided here | |
| 46 | 77 | func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| @@ -60,19 +91,22 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in | |||
| 60 | 91 | now := s.cfg.Now() | |
| 61 | 92 | ||
| 62 | 93 | rowsOnly := r.URL.Query().Has("rows") | |
| 63 | - | p := TreePage{} | |
| 94 | + | p := TreePage{Path: "/"} | |
| 95 | + | if t.path != "" { | |
| 96 | + | p.Path = visibleText(t.path) + "/" | |
| 97 | + | } | |
| 64 | 98 | for i, e := range entries { | |
| 65 | - | row := TreeRow{Glyph: "·", Name: visibleText(e.Name), Size: humanSize(e.Size), Cursor: i == 0 && !rowsOnly} | |
| 99 | + | row := TreeRow{Kind: kindFile, Name: visibleText(e.Name), Size: humanSize(e.Size), Cursor: i == 0 && !rowsOnly} | |
| 66 | 100 | switch e.Kind { | |
| 67 | 101 | case domain.EntryDirectory: | |
| 68 | - | row.Glyph, row.Name, row.Size = "▸", row.Name+"/", "-" | |
| 102 | + | row.Kind, row.Hint, row.Name, row.Size = kindDir, "directory", row.Name+"/", "" | |
| 69 | 103 | row.Href = repoHref(d.name(), "tree", t.name, e.Path) | |
| 70 | 104 | row.Expand = row.Href + "?rows" | |
| 71 | 105 | case domain.EntrySymlink: | |
| 72 | - | row.Glyph = "@" | |
| 106 | + | row.Kind, row.Hint = kindLink, "symbolic link" | |
| 73 | 107 | row.Href = repoHref(d.name(), "blob", t.name, e.Path) | |
| 74 | 108 | case domain.EntrySubmodule: | |
| 75 | - | row.Glyph, row.Size = "+", "-" | |
| 109 | + | row.Kind, row.Hint, row.Size = kindModule, "submodule", "" | |
| 76 | 110 | default: | |
| 77 | 111 | row.Href = repoHref(d.name(), "blob", t.name, e.Path) | |
| 78 | 112 | } | |
| @@ -82,10 +116,16 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in | |||
| 82 | 116 | } | |
| 83 | 117 | p.Rows = append(p.Rows, row) | |
| 84 | 118 | } | |
| 119 | + | for i := range p.Rows { | |
| 120 | + | p.Rows[i].Pos, p.Rows[i].Set = i+1, len(p.Rows) | |
| 121 | + | if res.Truncated { | |
| 122 | + | p.Rows[i].Set = -1 | |
| 123 | + | } | |
| 124 | + | } | |
| 85 | 125 | if rowsOnly { | |
| 86 | 126 | // The rows alone, for a directory opened in place (files.js). | |
| 87 | 127 | if res.Truncated { | |
| 88 | - | p.More = repoHref(d.name(), "tree", t.name, t.path) | |
| 128 | + | p.More, p.MorePos = repoHref(d.name(), "tree", t.name, t.path), len(p.Rows)+1 | |
| 89 | 129 | } | |
| 90 | 130 | return TreeRows(p), http.StatusOK, nil | |
| 91 | 131 | } | |
| @@ -95,12 +135,12 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in | |||
| 95 | 135 | if res.LastCommitsOmitted { | |
| 96 | 136 | p.Notes = append(p.Notes, "the last commit is shown for the first entries only.") | |
| 97 | 137 | } | |
| 98 | - | rd, err := s.readmeIn(ctx, d.repo.Name, res.CommitID.String(), t.name, t.path, entries) | |
| 99 | - | if err != nil { | |
| 100 | - | return nil, 0, err | |
| 101 | - | } | |
| 102 | - | if rd != nil { | |
| 103 | - | p.Readme = rd.readme | |
| 138 | + | if t.path != "" { | |
| 139 | + | up := path.Dir(t.path) | |
| 140 | + | if up == "." { | |
| 141 | + | up = "" | |
| 142 | + | } | |
| 143 | + | p.Up = repoHref(d.name(), "tree", t.name, up) | |
| 104 | 144 | } | |
| 105 | 145 | ||
| 106 | 146 | l := s.repoLayout(ctx, d, "tree") | |
| @@ -5,20 +5,33 @@ package web | |||||
| 5 | import ( | 5 | import ( | ||
| 6 | "context" | 6 | "context" | ||
| 7 | "net/http" | 7 | "net/http" | ||
| 8 | + | "path" | |||
| 8 | 9 | ||||
| 9 | "github.com/a-h/templ" | 10 | "github.com/a-h/templ" | ||
| 10 | 11 | ||||
| 11 | "bnema.dev/ermite/internal/domain" | 12 | "bnema.dev/ermite/internal/domain" | ||
| 12 | ) | 13 | ) | ||
| 13 | 14 | ||||
| 15 | + | // Entry kinds of a TreeRow; the stylesheet draws the node of a row by its kind. | |||
| 16 | + | const ( | |||
| 17 | + | kindDir = "dir" | |||
| 18 | + | kindFile = "file" | |||
| 19 | + | kindLink = "link" | |||
| 20 | + | kindModule = "module" | |||
| 21 | + | ) | |||
| 22 | + | ||||
| 14 | // TreeRow is one entry of a directory. | 23 | // TreeRow is one entry of a directory. | ||
| 15 | type TreeRow struct { | 24 | type TreeRow struct { | ||
| 16 | - | Glyph string // ▸ directory, @ symbolic link, + submodule, · file | 25 | + | Kind string // kindDir, kindFile, kindLink or kindModule |
| 17 | - | Name string | 26 | + | Name string |
| 18 | - | Href string // "" for a submodule | 27 | + | Href string // "" for a submodule |
| 19 | // Expand is the URL of the rows of a directory (Href?rows); "" for any | 28 | // Expand is the URL of the rows of a directory (Href?rows); "" for any | ||
| 20 | // other entry. | 29 | // other entry. | ||
| 21 | - | Expand string | 30 | + | Expand string |
| 31 | + | // Hint says what a directory, a link or a submodule is, in words, for the | |||
| 32 | + | // screen reader (the node drawn for it is aria-hidden) and the tooltip; "" | |||
| 33 | + | // for a file. | |||
| 34 | + | Hint string | |||
| 22 | Size string | 35 | Size string | ||
| 23 | Subject string | 36 | Subject string | ||
| 24 | CHref string | 37 | CHref string | ||
| @@ -26,21 +39,39 @@ type TreeRow struct { | |||||
| 26 | When string | 39 | When string | ||
| 27 | WhenAt string | 40 | WhenAt string | ||
| 28 | Cursor bool | 41 | Cursor bool | ||
| 42 | + | // Pos and Set are the aria-posinset and aria-setsize of the row among the | |||
| 43 | + | // entries of its directory; Set is -1 when the listing is cut, as the | |||
| 44 | + | // directory has more entries than are listed. | |||
| 45 | + | Pos, Set int | |||
| 29 | } | 46 | } | ||
| 30 | 47 | ||||
| 31 | // TreePage is a directory. | 48 | // TreePage is a directory. | ||
| 32 | type TreePage struct { | 49 | type TreePage struct { | ||
| 33 | - | Rows []TreeRow | 50 | + | Rows []TreeRow |
| 34 | - | Readme *Readme | 51 | + | // Path is the directory, "/" for the root. |
| 52 | + | Path string | |||
| 53 | + | // Up is the page of the parent directory, "" at the root: the h key of the | |||
| 54 | + | // tree goes there from a top-level entry. | |||
| 55 | + | Up string | |||
| 35 | // Notes say what the listing leaves out. | 56 | // Notes say what the listing leaves out. | ||
| 36 | Notes []string | 57 | Notes []string | ||
| 37 | // More, for the rows of a directory expanded in place, is the page of the | 58 | // More, for the rows of a directory expanded in place, is the page of the | ||
| 38 | // directory when it has more entries than are listed; "" otherwise. | 59 | // directory when it has more entries than are listed; "" otherwise. | ||
| 39 | More string | 60 | More string | ||
| 61 | + | // MorePos is the position of the row that says there are more. | |||
| 62 | + | MorePos int | |||
| 40 | } | 63 | } | ||
| 41 | 64 | ||||
| 42 | - | // tree lists a directory of a ref, with the last commit of each entry, and | 65 | + | // Unit is the noun of the entry count in the head of the tree. |
| 43 | - | // renders the README of the directory below. | 66 | + | func (p TreePage) Unit() string { |
| 67 | + | if len(p.Rows) == 1 { | |||
| 68 | + | return "entry" | |||
| 69 | + | } | |||
| 70 | + | return "entries" | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // tree lists a directory of a ref, with the last commit of each entry. With | |||
| 74 | + | // ?rows it answers the entries alone, for a directory opened in place. | |||
| 44 | // | 75 | // | ||
| 45 | //hexcheck:ignore suspicious-business-logic-in-adapter builds the directory page from the use case results; no rule is decided here | 76 | //hexcheck:ignore suspicious-business-logic-in-adapter builds the directory page from the use case results; no rule is decided here | ||
| 46 | func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, int, error) { | 77 | func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, int, error) { | ||
| @@ -60,19 +91,22 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in | |||||
| 60 | now := s.cfg.Now() | 91 | now := s.cfg.Now() | ||
| 61 | 92 | ||||
| 62 | rowsOnly := r.URL.Query().Has("rows") | 93 | rowsOnly := r.URL.Query().Has("rows") | ||
| 63 | - | p := TreePage{} | 94 | + | p := TreePage{Path: "/"} |
| 95 | + | if t.path != "" { | |||
| 96 | + | p.Path = visibleText(t.path) + "/" | |||
| 97 | + | } | |||
| 64 | for i, e := range entries { | 98 | for i, e := range entries { | ||
| 65 | - | row := TreeRow{Glyph: "·", Name: visibleText(e.Name), Size: humanSize(e.Size), Cursor: i == 0 && !rowsOnly} | 99 | + | row := TreeRow{Kind: kindFile, Name: visibleText(e.Name), Size: humanSize(e.Size), Cursor: i == 0 && !rowsOnly} |
| 66 | switch e.Kind { | 100 | switch e.Kind { | ||
| 67 | case domain.EntryDirectory: | 101 | case domain.EntryDirectory: | ||
| 68 | - | row.Glyph, row.Name, row.Size = "▸", row.Name+"/", "-" | 102 | + | row.Kind, row.Hint, row.Name, row.Size = kindDir, "directory", row.Name+"/", "" |
| 69 | row.Href = repoHref(d.name(), "tree", t.name, e.Path) | 103 | row.Href = repoHref(d.name(), "tree", t.name, e.Path) | ||
| 70 | row.Expand = row.Href + "?rows" | 104 | row.Expand = row.Href + "?rows" | ||
| 71 | case domain.EntrySymlink: | 105 | case domain.EntrySymlink: | ||
| 72 | - | row.Glyph = "@" | 106 | + | row.Kind, row.Hint = kindLink, "symbolic link" |
| 73 | row.Href = repoHref(d.name(), "blob", t.name, e.Path) | 107 | row.Href = repoHref(d.name(), "blob", t.name, e.Path) | ||
| 74 | case domain.EntrySubmodule: | 108 | case domain.EntrySubmodule: | ||
| 75 | - | row.Glyph, row.Size = "+", "-" | 109 | + | row.Kind, row.Hint, row.Size = kindModule, "submodule", "" |
| 76 | default: | 110 | default: | ||
| 77 | row.Href = repoHref(d.name(), "blob", t.name, e.Path) | 111 | row.Href = repoHref(d.name(), "blob", t.name, e.Path) | ||
| 78 | } | 112 | } | ||
| @@ -82,10 +116,16 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in | |||||
| 82 | } | 116 | } | ||
| 83 | p.Rows = append(p.Rows, row) | 117 | p.Rows = append(p.Rows, row) | ||
| 84 | } | 118 | } | ||
| 119 | + | for i := range p.Rows { | |||
| 120 | + | p.Rows[i].Pos, p.Rows[i].Set = i+1, len(p.Rows) | |||
| 121 | + | if res.Truncated { | |||
| 122 | + | p.Rows[i].Set = -1 | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 85 | if rowsOnly { | 125 | if rowsOnly { | ||
| 86 | // The rows alone, for a directory opened in place (files.js). | 126 | // The rows alone, for a directory opened in place (files.js). | ||
| 87 | if res.Truncated { | 127 | if res.Truncated { | ||
| 88 | - | p.More = repoHref(d.name(), "tree", t.name, t.path) | 128 | + | p.More, p.MorePos = repoHref(d.name(), "tree", t.name, t.path), len(p.Rows)+1 |
| 89 | } | 129 | } | ||
| 90 | return TreeRows(p), http.StatusOK, nil | 130 | return TreeRows(p), http.StatusOK, nil | ||
| 91 | } | 131 | } | ||
| @@ -95,12 +135,12 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in | |||||
| 95 | if res.LastCommitsOmitted { | 135 | if res.LastCommitsOmitted { | ||
| 96 | p.Notes = append(p.Notes, "the last commit is shown for the first entries only.") | 136 | p.Notes = append(p.Notes, "the last commit is shown for the first entries only.") | ||
| 97 | } | 137 | } | ||
| 98 | - | rd, err := s.readmeIn(ctx, d.repo.Name, res.CommitID.String(), t.name, t.path, entries) | 138 | + | if t.path != "" { |
| 99 | - | if err != nil { | 139 | + | up := path.Dir(t.path) |
| 100 | - | return nil, 0, err | 140 | + | if up == "." { |
| 101 | - | } | 141 | + | up = "" |
| 102 | - | if rd != nil { | 142 | + | } |
| 103 | - | p.Readme = rd.readme | 143 | + | p.Up = repoHref(d.name(), "tree", t.name, up) |
| 104 | } | 144 | } | ||
| 105 | 145 | ||||
| 106 | l := s.repoLayout(ctx, d, "tree") | 146 | l := s.repoLayout(ctx, d, "tree") | ||
M internal/adapters/web/tree.templ +99 -53
| @@ -2,50 +2,64 @@ | |||
| 2 | 2 | ||
| 3 | 3 | package web | |
| 4 | 4 | ||
| 5 | - | // Tree is a directory: the file finder, the entries with their last commit, the | |
| 6 | - | // notes on what is left out, and the README of the directory below. | |
| 5 | + | // Tree is a directory as a tree: the file finder on top, then the entries | |
| 6 | + | // with their last commit, and the notes on what is left out. A directory opens | |
| 7 | + | // in place (files.js); the keyboard walks the rows (j k l h, the arrows). | |
| 7 | 8 | templ Tree(l Layout, p TreePage) { | |
| 8 | 9 | @Shell(l, nil) { | |
| 9 | - | <div class="filter"> | |
| 10 | - | <label for="finder-q" class="sr">find a file</label> | |
| 11 | - | <input | |
| 12 | - | id="finder-q" | |
| 13 | - | type="search" | |
| 14 | - | data-filter | |
| 15 | - | role="combobox" | |
| 16 | - | aria-autocomplete="list" | |
| 17 | - | aria-controls="finder-list" | |
| 18 | - | aria-expanded="false" | |
| 19 | - | placeholder="Find a file…" | |
| 20 | - | autocomplete="off" | |
| 21 | - | spellcheck="false" | |
| 22 | - | hx-on:focus="ermiteFinder.load()" | |
| 23 | - | hx-on:input="ermiteFinder.input(this)" | |
| 24 | - | hx-on:keydown="ermiteFinder.key(this, event)" | |
| 25 | - | /> | |
| 10 | + | <div class="tree-find"> | |
| 11 | + | <div class="filter wide"> | |
| 12 | + | <label for="finder-q" class="sr">find a file</label> | |
| 13 | + | <input | |
| 14 | + | id="finder-q" | |
| 15 | + | type="search" | |
| 16 | + | data-filter | |
| 17 | + | role="combobox" | |
| 18 | + | aria-autocomplete="list" | |
| 19 | + | aria-controls="finder-list" | |
| 20 | + | aria-expanded="false" | |
| 21 | + | placeholder="Find a file in this repository…" | |
| 22 | + | autocomplete="off" | |
| 23 | + | spellcheck="false" | |
| 24 | + | hx-on:focus="ermiteFinder.load()" | |
| 25 | + | hx-on:input="ermiteFinder.input(this)" | |
| 26 | + | hx-on:keydown="ermiteFinder.key(this, event)" | |
| 27 | + | /> | |
| 28 | + | </div> | |
| 29 | + | <p class="tree-keys" data-js aria-hidden="true"><kbd>j</kbd> <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p> | |
| 26 | 30 | </div> | |
| 27 | - | <div id="finder" hidden> | |
| 31 | + | <div id="finder" class="tree-card" hidden> | |
| 28 | 32 | <p id="finder-status" class="dim" aria-live="polite"></p> | |
| 29 | 33 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"></ul> | |
| 30 | 34 | </div> | |
| 31 | - | <div id="tree-view"> | |
| 32 | - | <table class="list tree" aria-label="Entries"> | |
| 33 | - | <thead> | |
| 34 | - | <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr> | |
| 35 | - | </thead> | |
| 36 | - | <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)"> | |
| 37 | - | @TreeRows(p) | |
| 38 | - | </tbody> | |
| 39 | - | </table> | |
| 35 | + | <div id="tree-view" class="tree-card"> | |
| 36 | + | <div class="tree-head"> | |
| 37 | + | <span class="tree-path">{ p.Path }</span> | |
| 38 | + | if p.Up != "" { | |
| 39 | + | <a class="tree-up" href={ templ.SafeURL(p.Up) } title="parent directory (h)">..</a> | |
| 40 | + | } | |
| 41 | + | <span class="tree-count">{ itoa(len(p.Rows)) } { p.Unit() }</span> | |
| 42 | + | </div> | |
| 43 | + | <ul | |
| 44 | + | id="tree-rows" | |
| 45 | + | class="tree" | |
| 46 | + | role="tree" | |
| 47 | + | aria-label="Entries" | |
| 48 | + | data-rows | |
| 49 | + | data-tree | |
| 50 | + | if p.Up != "" { | |
| 51 | + | data-up={ p.Up } | |
| 52 | + | } | |
| 53 | + | hx-on:click="ermiteTree.click(event)" | |
| 54 | + | > | |
| 55 | + | @TreeRows(p) | |
| 56 | + | </ul> | |
| 40 | 57 | if len(p.Rows) == 0 { | |
| 41 | 58 | <p class="lede dim">this directory is empty.</p> | |
| 42 | 59 | } | |
| 43 | 60 | for _, n := range p.Notes { | |
| 44 | 61 | <p class="note">{ n }</p> | |
| 45 | 62 | } | |
| 46 | - | if p.Readme != nil { | |
| 47 | - | @ReadmeCard(*p.Readme) | |
| 48 | - | } | |
| 49 | 63 | </div> | |
| 50 | 64 | } | |
| 51 | 65 | } | |
| @@ -58,41 +72,73 @@ templ TreeRows(p TreePage) { | |||
| 58 | 72 | @TreeRowView(r) | |
| 59 | 73 | } | |
| 60 | 74 | if p.More != "" { | |
| 61 | - | <tr class="tree-more"><td colspan="7"><a href={ templ.SafeURL(p.More) }>more entries: open the directory</a></td></tr> | |
| 75 | + | <li class="tree-more" role="treeitem" aria-level="1" aria-posinset={ itoa(p.MorePos) } aria-setsize="-1" data-depth="0"> | |
| 76 | + | <span class="t-main"> | |
| 77 | + | <span class="t-twig" aria-hidden="true"></span> | |
| 78 | + | <span class="t-node" aria-hidden="true"></span> | |
| 79 | + | <a class="t-name" href={ templ.SafeURL(p.More) }>more entries: open the directory</a> | |
| 80 | + | </span> | |
| 81 | + | </li> | |
| 62 | 82 | } | |
| 63 | 83 | } | |
| 64 | 84 | ||
| 65 | - | // TreeRowView is one entry of a directory. Glyph: ▸ directory (▾ once opened | |
| 66 | - | // in place), @ symbolic link, + submodule, · file. A directory link carries | |
| 67 | - | // data-expand, the URL of its rows. | |
| 85 | + | // TreeRowView is one entry of a directory, a row of the tree: the guides of | |
| 86 | + | // its depth (t-twig), its node, its name, then its last commit. A directory | |
| 87 | + | // carries data-expand, the URL of its rows, and aria-expanded; files.js sets | |
| 88 | + | // the depth of the rows it inserts (data-depth, --depth, aria-level). | |
| 89 | + | // aria-posinset and aria-setsize count the entries of the row's own directory | |
| 90 | + | // (setsize -1: the listing is cut and the total unknown), so the positions | |
| 91 | + | // stay right once the rows of a directory are inserted into the flat list. The | |
| 92 | + | // node is drawn by the stylesheet from data-kind, which is aria-hidden: the | |
| 93 | + | // type of a directory, a link or a submodule is said in text for screen readers | |
| 94 | + | // (the .sr span, part of the name of the treeitem) and in a tooltip. | |
| 68 | 95 | templ TreeRowView(r TreeRow) { | |
| 69 | - | <tr data-cursor?={ r.Cursor }> | |
| 70 | - | <td class="cur" aria-hidden="true"></td> | |
| 71 | - | <td class="c-kind" aria-hidden="true">{ r.Glyph }</td> | |
| 72 | - | <td class="c-name"> | |
| 96 | + | <li | |
| 97 | + | role="treeitem" | |
| 98 | + | aria-level="1" | |
| 99 | + | aria-posinset={ itoa(r.Pos) } | |
| 100 | + | aria-setsize={ itoa(r.Set) } | |
| 101 | + | data-kind={ r.Kind } | |
| 102 | + | data-depth="0" | |
| 103 | + | data-cursor?={ r.Cursor } | |
| 104 | + | if r.Expand != "" { | |
| 105 | + | data-expand={ r.Expand } | |
| 106 | + | aria-expanded="false" | |
| 107 | + | } | |
| 108 | + | > | |
| 109 | + | <span class="t-main"> | |
| 110 | + | <span class="t-twig" aria-hidden="true"></span> | |
| 111 | + | <span class="t-node" aria-hidden="true"></span> | |
| 73 | 112 | if r.Href != "" { | |
| 74 | 113 | <a | |
| 114 | + | class="t-name" | |
| 75 | 115 | href={ templ.SafeURL(r.Href) } | |
| 76 | - | if r.Expand != "" { | |
| 77 | - | data-expand={ r.Expand } | |
| 78 | - | aria-expanded="false" | |
| 116 | + | if r.Hint != "" { | |
| 117 | + | title={ r.Hint } | |
| 79 | 118 | } | |
| 80 | 119 | >{ r.Name }</a> | |
| 81 | 120 | } else { | |
| 82 | - | { r.Name } | |
| 121 | + | <span | |
| 122 | + | class="t-name" | |
| 123 | + | if r.Hint != "" { | |
| 124 | + | title={ r.Hint } | |
| 125 | + | } | |
| 126 | + | >{ r.Name }</span> | |
| 127 | + | } | |
| 128 | + | if r.Hint != "" { | |
| 129 | + | <span class="sr">{ " (" + r.Hint + ")" }</span> | |
| 83 | 130 | } | |
| 84 | - | </td> | |
| 85 | - | <td class="c-size num dim">{ r.Size }</td> | |
| 86 | - | <td class="c-sub dim"> | |
| 131 | + | </span> | |
| 132 | + | <span class="t-size">{ r.Size }</span> | |
| 133 | + | <span class="t-sub"> | |
| 87 | 134 | if r.CHref != "" { | |
| 88 | - | <a href={ templ.SafeURL(r.CHref) }>{ r.Subject }</a> | |
| 135 | + | <a class="t-commit" href={ templ.SafeURL(r.CHref) } title={ r.Author }>{ r.Subject }</a> | |
| 89 | 136 | } | |
| 90 | - | </td> | |
| 91 | - | <td class="c-who dim">{ r.Author }</td> | |
| 92 | - | <td class="c-when nowrap dim"> | |
| 137 | + | </span> | |
| 138 | + | <span class="t-when"> | |
| 93 | 139 | if r.WhenAt != "" { | |
| 94 | 140 | <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time> | |
| 95 | 141 | } | |
| 96 | - | </td> | |
| 97 | - | </tr> | |
| 142 | + | </span> | |
| 143 | + | </li> | |
| 98 | 144 | } | |
| @@ -2,50 +2,64 @@ | |||||
| 2 | 2 | ||||
| 3 | package web | 3 | package web | ||
| 4 | 4 | ||||
| 5 | - | // Tree is a directory: the file finder, the entries with their last commit, the | 5 | + | // Tree is a directory as a tree: the file finder on top, then the entries |
| 6 | - | // notes on what is left out, and the README of the directory below. | 6 | + | // with their last commit, and the notes on what is left out. A directory opens |
| 7 | + | // in place (files.js); the keyboard walks the rows (j k l h, the arrows). | |||
| 7 | templ Tree(l Layout, p TreePage) { | 8 | templ Tree(l Layout, p TreePage) { | ||
| 8 | @Shell(l, nil) { | 9 | @Shell(l, nil) { | ||
| 9 | - | <div class="filter"> | 10 | + | <div class="tree-find"> |
| 10 | - | <label for="finder-q" class="sr">find a file</label> | 11 | + | <div class="filter wide"> |
| 11 | - | <input | 12 | + | <label for="finder-q" class="sr">find a file</label> |
| 12 | - | id="finder-q" | 13 | + | <input |
| 13 | - | type="search" | 14 | + | id="finder-q" |
| 14 | - | data-filter | 15 | + | type="search" |
| 15 | - | role="combobox" | 16 | + | data-filter |
| 16 | - | aria-autocomplete="list" | 17 | + | role="combobox" |
| 17 | - | aria-controls="finder-list" | 18 | + | aria-autocomplete="list" |
| 18 | - | aria-expanded="false" | 19 | + | aria-controls="finder-list" |
| 19 | - | placeholder="Find a file…" | 20 | + | aria-expanded="false" |
| 20 | - | autocomplete="off" | 21 | + | placeholder="Find a file in this repository…" |
| 21 | - | spellcheck="false" | 22 | + | autocomplete="off" |
| 22 | - | hx-on:focus="ermiteFinder.load()" | 23 | + | spellcheck="false" |
| 23 | - | hx-on:input="ermiteFinder.input(this)" | 24 | + | hx-on:focus="ermiteFinder.load()" |
| 24 | - | hx-on:keydown="ermiteFinder.key(this, event)" | 25 | + | hx-on:input="ermiteFinder.input(this)" |
| 25 | - | /> | 26 | + | hx-on:keydown="ermiteFinder.key(this, event)" |
| 27 | + | /> | |||
| 28 | + | </div> | |||
| 29 | + | <p class="tree-keys" data-js aria-hidden="true"><kbd>j</kbd> <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p> | |||
| 26 | </div> | 30 | </div> | ||
| 27 | - | <div id="finder" hidden> | 31 | + | <div id="finder" class="tree-card" hidden> |
| 28 | <p id="finder-status" class="dim" aria-live="polite"></p> | 32 | <p id="finder-status" class="dim" aria-live="polite"></p> | ||
| 29 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"></ul> | 33 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"></ul> | ||
| 30 | </div> | 34 | </div> | ||
| 31 | - | <div id="tree-view"> | 35 | + | <div id="tree-view" class="tree-card"> |
| 32 | - | <table class="list tree" aria-label="Entries"> | 36 | + | <div class="tree-head"> |
| 33 | - | <thead> | 37 | + | <span class="tree-path">{ p.Path }</span> |
| 34 | - | <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr> | 38 | + | if p.Up != "" { |
| 35 | - | </thead> | 39 | + | <a class="tree-up" href={ templ.SafeURL(p.Up) } title="parent directory (h)">..</a> |
| 36 | - | <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)"> | 40 | + | } |
| 37 | - | @TreeRows(p) | 41 | + | <span class="tree-count">{ itoa(len(p.Rows)) } { p.Unit() }</span> |
| 38 | - | </tbody> | 42 | + | </div> |
| 39 | - | </table> | 43 | + | <ul |
| 44 | + | id="tree-rows" | |||
| 45 | + | class="tree" | |||
| 46 | + | role="tree" | |||
| 47 | + | aria-label="Entries" | |||
| 48 | + | data-rows | |||
| 49 | + | data-tree | |||
| 50 | + | if p.Up != "" { | |||
| 51 | + | data-up={ p.Up } | |||
| 52 | + | } | |||
| 53 | + | hx-on:click="ermiteTree.click(event)" | |||
| 54 | + | > | |||
| 55 | + | @TreeRows(p) | |||
| 56 | + | </ul> | |||
| 40 | if len(p.Rows) == 0 { | 57 | if len(p.Rows) == 0 { | ||
| 41 | <p class="lede dim">this directory is empty.</p> | 58 | <p class="lede dim">this directory is empty.</p> | ||
| 42 | } | 59 | } | ||
| 43 | for _, n := range p.Notes { | 60 | for _, n := range p.Notes { | ||
| 44 | <p class="note">{ n }</p> | 61 | <p class="note">{ n }</p> | ||
| 45 | } | 62 | } | ||
| 46 | - | if p.Readme != nil { | |||
| 47 | - | @ReadmeCard(*p.Readme) | |||
| 48 | - | } | |||
| 49 | </div> | 63 | </div> | ||
| 50 | } | 64 | } | ||
| 51 | } | 65 | } | ||
| @@ -58,41 +72,73 @@ templ TreeRows(p TreePage) { | |||||
| 58 | @TreeRowView(r) | 72 | @TreeRowView(r) | ||
| 59 | } | 73 | } | ||
| 60 | if p.More != "" { | 74 | if p.More != "" { | ||
| 61 | - | <tr class="tree-more"><td colspan="7"><a href={ templ.SafeURL(p.More) }>more entries: open the directory</a></td></tr> | 75 | + | <li class="tree-more" role="treeitem" aria-level="1" aria-posinset={ itoa(p.MorePos) } aria-setsize="-1" data-depth="0"> |
| 76 | + | <span class="t-main"> | |||
| 77 | + | <span class="t-twig" aria-hidden="true"></span> | |||
| 78 | + | <span class="t-node" aria-hidden="true"></span> | |||
| 79 | + | <a class="t-name" href={ templ.SafeURL(p.More) }>more entries: open the directory</a> | |||
| 80 | + | </span> | |||
| 81 | + | </li> | |||
| 62 | } | 82 | } | ||
| 63 | } | 83 | } | ||
| 64 | 84 | ||||
| 65 | - | // TreeRowView is one entry of a directory. Glyph: ▸ directory (▾ once opened | 85 | + | // TreeRowView is one entry of a directory, a row of the tree: the guides of |
| 66 | - | // in place), @ symbolic link, + submodule, · file. A directory link carries | 86 | + | // its depth (t-twig), its node, its name, then its last commit. A directory |
| 67 | - | // data-expand, the URL of its rows. | 87 | + | // carries data-expand, the URL of its rows, and aria-expanded; files.js sets |
| 88 | + | // the depth of the rows it inserts (data-depth, --depth, aria-level). | |||
| 89 | + | // aria-posinset and aria-setsize count the entries of the row's own directory | |||
| 90 | + | // (setsize -1: the listing is cut and the total unknown), so the positions | |||
| 91 | + | // stay right once the rows of a directory are inserted into the flat list. The | |||
| 92 | + | // node is drawn by the stylesheet from data-kind, which is aria-hidden: the | |||
| 93 | + | // type of a directory, a link or a submodule is said in text for screen readers | |||
| 94 | + | // (the .sr span, part of the name of the treeitem) and in a tooltip. | |||
| 68 | templ TreeRowView(r TreeRow) { | 95 | templ TreeRowView(r TreeRow) { | ||
| 69 | - | <tr data-cursor?={ r.Cursor }> | 96 | + | <li |
| 70 | - | <td class="cur" aria-hidden="true"></td> | 97 | + | role="treeitem" |
| 71 | - | <td class="c-kind" aria-hidden="true">{ r.Glyph }</td> | 98 | + | aria-level="1" |
| 72 | - | <td class="c-name"> | 99 | + | aria-posinset={ itoa(r.Pos) } |
| 100 | + | aria-setsize={ itoa(r.Set) } | |||
| 101 | + | data-kind={ r.Kind } | |||
| 102 | + | data-depth="0" | |||
| 103 | + | data-cursor?={ r.Cursor } | |||
| 104 | + | if r.Expand != "" { | |||
| 105 | + | data-expand={ r.Expand } | |||
| 106 | + | aria-expanded="false" | |||
| 107 | + | } | |||
| 108 | + | > | |||
| 109 | + | <span class="t-main"> | |||
| 110 | + | <span class="t-twig" aria-hidden="true"></span> | |||
| 111 | + | <span class="t-node" aria-hidden="true"></span> | |||
| 73 | if r.Href != "" { | 112 | if r.Href != "" { | ||
| 74 | <a | 113 | <a | ||
| 114 | + | class="t-name" | |||
| 75 | href={ templ.SafeURL(r.Href) } | 115 | href={ templ.SafeURL(r.Href) } | ||
| 76 | - | if r.Expand != "" { | 116 | + | if r.Hint != "" { |
| 77 | - | data-expand={ r.Expand } | 117 | + | title={ r.Hint } |
| 78 | - | aria-expanded="false" | |||
| 79 | } | 118 | } | ||
| 80 | >{ r.Name }</a> | 119 | >{ r.Name }</a> | ||
| 81 | } else { | 120 | } else { | ||
| 82 | - | { r.Name } | 121 | + | <span |
| 122 | + | class="t-name" | |||
| 123 | + | if r.Hint != "" { | |||
| 124 | + | title={ r.Hint } | |||
| 125 | + | } | |||
| 126 | + | >{ r.Name }</span> | |||
| 127 | + | } | |||
| 128 | + | if r.Hint != "" { | |||
| 129 | + | <span class="sr">{ " (" + r.Hint + ")" }</span> | |||
| 83 | } | 130 | } | ||
| 84 | - | </td> | 131 | + | </span> |
| 85 | - | <td class="c-size num dim">{ r.Size }</td> | 132 | + | <span class="t-size">{ r.Size }</span> |
| 86 | - | <td class="c-sub dim"> | 133 | + | <span class="t-sub"> |
| 87 | if r.CHref != "" { | 134 | if r.CHref != "" { | ||
| 88 | - | <a href={ templ.SafeURL(r.CHref) }>{ r.Subject }</a> | 135 | + | <a class="t-commit" href={ templ.SafeURL(r.CHref) } title={ r.Author }>{ r.Subject }</a> |
| 89 | } | 136 | } | ||
| 90 | - | </td> | 137 | + | </span> |
| 91 | - | <td class="c-who dim">{ r.Author }</td> | 138 | + | <span class="t-when"> |
| 92 | - | <td class="c-when nowrap dim"> | |||
| 93 | if r.WhenAt != "" { | 139 | if r.WhenAt != "" { | ||
| 94 | <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time> | 140 | <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time> | ||
| 95 | } | 141 | } | ||
| 96 | - | </td> | 142 | + | </span> |
| 97 | - | </tr> | 143 | + | </li> |
| 98 | } | 144 | } | ||
M internal/adapters/web/tree_templ.go +310 -115 generated file: collapsed
M internal/app/testdata/web/blob-binary.html +14 -3
| @@ -174,14 +174,25 @@ | |||
| 174 | 174 | <dt> | |
| 175 | 175 | <kbd>enter</kbd> | |
| 176 | 176 | </dt> | |
| 177 | - | <dd>open the row under the cursor</dd> | |
| 177 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 178 | + | </div> | |
| 179 | + | <div> | |
| 180 | + | <dt> | |
| 181 | + | <kbd>↑</kbd> | |
| 182 | + | <kbd>↓</kbd> | |
| 183 | + | <kbd>←</kbd> | |
| 184 | + | <kbd>→</kbd> | |
| 185 | + | <kbd>home</kbd> | |
| 186 | + | <kbd>end</kbd> | |
| 187 | + | </dt> | |
| 188 | + | <dd>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</dd> | |
| 178 | 189 | </div> | |
| 179 | 190 | <div> | |
| 180 | 191 | <dt> | |
| 181 | 192 | <kbd>l</kbd> | |
| 182 | 193 | <kbd>h</kbd> | |
| 183 | 194 | </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> | |
| 195 | + | <dd>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</dd> | |
| 185 | 196 | </div> | |
| 186 | 197 | <div> | |
| 187 | 198 | <dt> | |
| @@ -194,7 +205,7 @@ | |||
| 194 | 205 | <dt> | |
| 195 | 206 | <kbd>/</kbd> | |
| 196 | 207 | </dt> | |
| 197 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 208 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 198 | 209 | </div> | |
| 199 | 210 | <div> | |
| 200 | 211 | <dt> | |
| @@ -174,14 +174,25 @@ | |||||
| 174 | <dt> | 174 | <dt> | ||
| 175 | <kbd>enter</kbd> | 175 | <kbd>enter</kbd> | ||
| 176 | </dt> | 176 | </dt> | ||
| 177 | - | <dd>open the row under the cursor</dd> | 177 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 178 | + | </div> | |||
| 179 | + | <div> | |||
| 180 | + | <dt> | |||
| 181 | + | <kbd>↑</kbd> | |||
| 182 | + | <kbd>↓</kbd> | |||
| 183 | + | <kbd>←</kbd> | |||
| 184 | + | <kbd>→</kbd> | |||
| 185 | + | <kbd>home</kbd> | |||
| 186 | + | <kbd>end</kbd> | |||
| 187 | + | </dt> | |||
| 188 | + | <dd>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</dd> | |||
| 178 | </div> | 189 | </div> | ||
| 179 | <div> | 190 | <div> | ||
| 180 | <dt> | 191 | <dt> | ||
| 181 | <kbd>l</kbd> | 192 | <kbd>l</kbd> | ||
| 182 | <kbd>h</kbd> | 193 | <kbd>h</kbd> | ||
| 183 | </dt> | 194 | </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> | 195 | + | <dd>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</dd> |
| 185 | </div> | 196 | </div> | ||
| 186 | <div> | 197 | <div> | ||
| 187 | <dt> | 198 | <dt> | ||
| @@ -194,7 +205,7 @@ | |||||
| 194 | <dt> | 205 | <dt> | ||
| 195 | <kbd>/</kbd> | 206 | <kbd>/</kbd> | ||
| 196 | </dt> | 207 | </dt> | ||
| 197 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 208 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 198 | </div> | 209 | </div> | ||
| 199 | <div> | 210 | <div> | ||
| 200 | <dt> | 211 | <dt> | ||
M internal/app/testdata/web/blob-code.html +14 -3
| @@ -244,14 +244,25 @@ | |||
| 244 | 244 | <dt> | |
| 245 | 245 | <kbd>enter</kbd> | |
| 246 | 246 | </dt> | |
| 247 | - | <dd>open the row under the cursor</dd> | |
| 247 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 248 | + | </div> | |
| 249 | + | <div> | |
| 250 | + | <dt> | |
| 251 | + | <kbd>↑</kbd> | |
| 252 | + | <kbd>↓</kbd> | |
| 253 | + | <kbd>←</kbd> | |
| 254 | + | <kbd>→</kbd> | |
| 255 | + | <kbd>home</kbd> | |
| 256 | + | <kbd>end</kbd> | |
| 257 | + | </dt> | |
| 258 | + | <dd>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</dd> | |
| 248 | 259 | </div> | |
| 249 | 260 | <div> | |
| 250 | 261 | <dt> | |
| 251 | 262 | <kbd>l</kbd> | |
| 252 | 263 | <kbd>h</kbd> | |
| 253 | 264 | </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> | |
| 265 | + | <dd>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</dd> | |
| 255 | 266 | </div> | |
| 256 | 267 | <div> | |
| 257 | 268 | <dt> | |
| @@ -264,7 +275,7 @@ | |||
| 264 | 275 | <dt> | |
| 265 | 276 | <kbd>/</kbd> | |
| 266 | 277 | </dt> | |
| 267 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 278 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 268 | 279 | </div> | |
| 269 | 280 | <div> | |
| 270 | 281 | <dt> | |
| @@ -244,14 +244,25 @@ | |||||
| 244 | <dt> | 244 | <dt> | ||
| 245 | <kbd>enter</kbd> | 245 | <kbd>enter</kbd> | ||
| 246 | </dt> | 246 | </dt> | ||
| 247 | - | <dd>open the row under the cursor</dd> | 247 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 248 | + | </div> | |||
| 249 | + | <div> | |||
| 250 | + | <dt> | |||
| 251 | + | <kbd>↑</kbd> | |||
| 252 | + | <kbd>↓</kbd> | |||
| 253 | + | <kbd>←</kbd> | |||
| 254 | + | <kbd>→</kbd> | |||
| 255 | + | <kbd>home</kbd> | |||
| 256 | + | <kbd>end</kbd> | |||
| 257 | + | </dt> | |||
| 258 | + | <dd>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</dd> | |||
| 248 | </div> | 259 | </div> | ||
| 249 | <div> | 260 | <div> | ||
| 250 | <dt> | 261 | <dt> | ||
| 251 | <kbd>l</kbd> | 262 | <kbd>l</kbd> | ||
| 252 | <kbd>h</kbd> | 263 | <kbd>h</kbd> | ||
| 253 | </dt> | 264 | </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> | 265 | + | <dd>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</dd> |
| 255 | </div> | 266 | </div> | ||
| 256 | <div> | 267 | <div> | ||
| 257 | <dt> | 268 | <dt> | ||
| @@ -264,7 +275,7 @@ | |||||
| 264 | <dt> | 275 | <dt> | ||
| 265 | <kbd>/</kbd> | 276 | <kbd>/</kbd> | ||
| 266 | </dt> | 277 | </dt> | ||
| 267 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 278 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 268 | </div> | 279 | </div> | ||
| 269 | <div> | 280 | <div> | ||
| 270 | <dt> | 281 | <dt> | ||
M internal/app/testdata/web/blob-empty.html +14 -3
| @@ -174,14 +174,25 @@ | |||
| 174 | 174 | <dt> | |
| 175 | 175 | <kbd>enter</kbd> | |
| 176 | 176 | </dt> | |
| 177 | - | <dd>open the row under the cursor</dd> | |
| 177 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 178 | + | </div> | |
| 179 | + | <div> | |
| 180 | + | <dt> | |
| 181 | + | <kbd>↑</kbd> | |
| 182 | + | <kbd>↓</kbd> | |
| 183 | + | <kbd>←</kbd> | |
| 184 | + | <kbd>→</kbd> | |
| 185 | + | <kbd>home</kbd> | |
| 186 | + | <kbd>end</kbd> | |
| 187 | + | </dt> | |
| 188 | + | <dd>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</dd> | |
| 178 | 189 | </div> | |
| 179 | 190 | <div> | |
| 180 | 191 | <dt> | |
| 181 | 192 | <kbd>l</kbd> | |
| 182 | 193 | <kbd>h</kbd> | |
| 183 | 194 | </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> | |
| 195 | + | <dd>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</dd> | |
| 185 | 196 | </div> | |
| 186 | 197 | <div> | |
| 187 | 198 | <dt> | |
| @@ -194,7 +205,7 @@ | |||
| 194 | 205 | <dt> | |
| 195 | 206 | <kbd>/</kbd> | |
| 196 | 207 | </dt> | |
| 197 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 208 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 198 | 209 | </div> | |
| 199 | 210 | <div> | |
| 200 | 211 | <dt> | |
| @@ -174,14 +174,25 @@ | |||||
| 174 | <dt> | 174 | <dt> | ||
| 175 | <kbd>enter</kbd> | 175 | <kbd>enter</kbd> | ||
| 176 | </dt> | 176 | </dt> | ||
| 177 | - | <dd>open the row under the cursor</dd> | 177 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 178 | + | </div> | |||
| 179 | + | <div> | |||
| 180 | + | <dt> | |||
| 181 | + | <kbd>↑</kbd> | |||
| 182 | + | <kbd>↓</kbd> | |||
| 183 | + | <kbd>←</kbd> | |||
| 184 | + | <kbd>→</kbd> | |||
| 185 | + | <kbd>home</kbd> | |||
| 186 | + | <kbd>end</kbd> | |||
| 187 | + | </dt> | |||
| 188 | + | <dd>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</dd> | |||
| 178 | </div> | 189 | </div> | ||
| 179 | <div> | 190 | <div> | ||
| 180 | <dt> | 191 | <dt> | ||
| 181 | <kbd>l</kbd> | 192 | <kbd>l</kbd> | ||
| 182 | <kbd>h</kbd> | 193 | <kbd>h</kbd> | ||
| 183 | </dt> | 194 | </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> | 195 | + | <dd>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</dd> |
| 185 | </div> | 196 | </div> | ||
| 186 | <div> | 197 | <div> | ||
| 187 | <dt> | 198 | <dt> | ||
| @@ -194,7 +205,7 @@ | |||||
| 194 | <dt> | 205 | <dt> | ||
| 195 | <kbd>/</kbd> | 206 | <kbd>/</kbd> | ||
| 196 | </dt> | 207 | </dt> | ||
| 197 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 208 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 198 | </div> | 209 | </div> | ||
| 199 | <div> | 210 | <div> | ||
| 200 | <dt> | 211 | <dt> | ||
M internal/app/testdata/web/blob-image.html +14 -3
| @@ -176,14 +176,25 @@ | |||
| 176 | 176 | <dt> | |
| 177 | 177 | <kbd>enter</kbd> | |
| 178 | 178 | </dt> | |
| 179 | - | <dd>open the row under the cursor</dd> | |
| 179 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 180 | + | </div> | |
| 181 | + | <div> | |
| 182 | + | <dt> | |
| 183 | + | <kbd>↑</kbd> | |
| 184 | + | <kbd>↓</kbd> | |
| 185 | + | <kbd>←</kbd> | |
| 186 | + | <kbd>→</kbd> | |
| 187 | + | <kbd>home</kbd> | |
| 188 | + | <kbd>end</kbd> | |
| 189 | + | </dt> | |
| 190 | + | <dd>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</dd> | |
| 180 | 191 | </div> | |
| 181 | 192 | <div> | |
| 182 | 193 | <dt> | |
| 183 | 194 | <kbd>l</kbd> | |
| 184 | 195 | <kbd>h</kbd> | |
| 185 | 196 | </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> | |
| 197 | + | <dd>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</dd> | |
| 187 | 198 | </div> | |
| 188 | 199 | <div> | |
| 189 | 200 | <dt> | |
| @@ -196,7 +207,7 @@ | |||
| 196 | 207 | <dt> | |
| 197 | 208 | <kbd>/</kbd> | |
| 198 | 209 | </dt> | |
| 199 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 210 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 200 | 211 | </div> | |
| 201 | 212 | <div> | |
| 202 | 213 | <dt> | |
| @@ -176,14 +176,25 @@ | |||||
| 176 | <dt> | 176 | <dt> | ||
| 177 | <kbd>enter</kbd> | 177 | <kbd>enter</kbd> | ||
| 178 | </dt> | 178 | </dt> | ||
| 179 | - | <dd>open the row under the cursor</dd> | 179 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 180 | + | </div> | |||
| 181 | + | <div> | |||
| 182 | + | <dt> | |||
| 183 | + | <kbd>↑</kbd> | |||
| 184 | + | <kbd>↓</kbd> | |||
| 185 | + | <kbd>←</kbd> | |||
| 186 | + | <kbd>→</kbd> | |||
| 187 | + | <kbd>home</kbd> | |||
| 188 | + | <kbd>end</kbd> | |||
| 189 | + | </dt> | |||
| 190 | + | <dd>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</dd> | |||
| 180 | </div> | 191 | </div> | ||
| 181 | <div> | 192 | <div> | ||
| 182 | <dt> | 193 | <dt> | ||
| 183 | <kbd>l</kbd> | 194 | <kbd>l</kbd> | ||
| 184 | <kbd>h</kbd> | 195 | <kbd>h</kbd> | ||
| 185 | </dt> | 196 | </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> | 197 | + | <dd>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</dd> |
| 187 | </div> | 198 | </div> | ||
| 188 | <div> | 199 | <div> | ||
| 189 | <dt> | 200 | <dt> | ||
| @@ -196,7 +207,7 @@ | |||||
| 196 | <dt> | 207 | <dt> | ||
| 197 | <kbd>/</kbd> | 208 | <kbd>/</kbd> | ||
| 198 | </dt> | 209 | </dt> | ||
| 199 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 210 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 200 | </div> | 211 | </div> | ||
| 201 | <div> | 212 | <div> | ||
| 202 | <dt> | 213 | <dt> | ||
M internal/app/testdata/web/blob-markdown.html +14 -3
| @@ -241,14 +241,25 @@ | |||
| 241 | 241 | <dt> | |
| 242 | 242 | <kbd>enter</kbd> | |
| 243 | 243 | </dt> | |
| 244 | - | <dd>open the row under the cursor</dd> | |
| 244 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 245 | + | </div> | |
| 246 | + | <div> | |
| 247 | + | <dt> | |
| 248 | + | <kbd>↑</kbd> | |
| 249 | + | <kbd>↓</kbd> | |
| 250 | + | <kbd>←</kbd> | |
| 251 | + | <kbd>→</kbd> | |
| 252 | + | <kbd>home</kbd> | |
| 253 | + | <kbd>end</kbd> | |
| 254 | + | </dt> | |
| 255 | + | <dd>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</dd> | |
| 245 | 256 | </div> | |
| 246 | 257 | <div> | |
| 247 | 258 | <dt> | |
| 248 | 259 | <kbd>l</kbd> | |
| 249 | 260 | <kbd>h</kbd> | |
| 250 | 261 | </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> | |
| 262 | + | <dd>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</dd> | |
| 252 | 263 | </div> | |
| 253 | 264 | <div> | |
| 254 | 265 | <dt> | |
| @@ -261,7 +272,7 @@ | |||
| 261 | 272 | <dt> | |
| 262 | 273 | <kbd>/</kbd> | |
| 263 | 274 | </dt> | |
| 264 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 275 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 265 | 276 | </div> | |
| 266 | 277 | <div> | |
| 267 | 278 | <dt> | |
| @@ -241,14 +241,25 @@ | |||||
| 241 | <dt> | 241 | <dt> | ||
| 242 | <kbd>enter</kbd> | 242 | <kbd>enter</kbd> | ||
| 243 | </dt> | 243 | </dt> | ||
| 244 | - | <dd>open the row under the cursor</dd> | 244 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 245 | + | </div> | |||
| 246 | + | <div> | |||
| 247 | + | <dt> | |||
| 248 | + | <kbd>↑</kbd> | |||
| 249 | + | <kbd>↓</kbd> | |||
| 250 | + | <kbd>←</kbd> | |||
| 251 | + | <kbd>→</kbd> | |||
| 252 | + | <kbd>home</kbd> | |||
| 253 | + | <kbd>end</kbd> | |||
| 254 | + | </dt> | |||
| 255 | + | <dd>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</dd> | |||
| 245 | </div> | 256 | </div> | ||
| 246 | <div> | 257 | <div> | ||
| 247 | <dt> | 258 | <dt> | ||
| 248 | <kbd>l</kbd> | 259 | <kbd>l</kbd> | ||
| 249 | <kbd>h</kbd> | 260 | <kbd>h</kbd> | ||
| 250 | </dt> | 261 | </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> | 262 | + | <dd>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</dd> |
| 252 | </div> | 263 | </div> | ||
| 253 | <div> | 264 | <div> | ||
| 254 | <dt> | 265 | <dt> | ||
| @@ -261,7 +272,7 @@ | |||||
| 261 | <dt> | 272 | <dt> | ||
| 262 | <kbd>/</kbd> | 273 | <kbd>/</kbd> | ||
| 263 | </dt> | 274 | </dt> | ||
| 264 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 275 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 265 | </div> | 276 | </div> | ||
| 266 | <div> | 277 | <div> | ||
| 267 | <dt> | 278 | <dt> | ||
M internal/app/testdata/web/blob-svg.html +14 -3
| @@ -199,14 +199,25 @@ | |||
| 199 | 199 | <dt> | |
| 200 | 200 | <kbd>enter</kbd> | |
| 201 | 201 | </dt> | |
| 202 | - | <dd>open the row under the cursor</dd> | |
| 202 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 203 | + | </div> | |
| 204 | + | <div> | |
| 205 | + | <dt> | |
| 206 | + | <kbd>↑</kbd> | |
| 207 | + | <kbd>↓</kbd> | |
| 208 | + | <kbd>←</kbd> | |
| 209 | + | <kbd>→</kbd> | |
| 210 | + | <kbd>home</kbd> | |
| 211 | + | <kbd>end</kbd> | |
| 212 | + | </dt> | |
| 213 | + | <dd>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</dd> | |
| 203 | 214 | </div> | |
| 204 | 215 | <div> | |
| 205 | 216 | <dt> | |
| 206 | 217 | <kbd>l</kbd> | |
| 207 | 218 | <kbd>h</kbd> | |
| 208 | 219 | </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> | |
| 220 | + | <dd>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</dd> | |
| 210 | 221 | </div> | |
| 211 | 222 | <div> | |
| 212 | 223 | <dt> | |
| @@ -219,7 +230,7 @@ | |||
| 219 | 230 | <dt> | |
| 220 | 231 | <kbd>/</kbd> | |
| 221 | 232 | </dt> | |
| 222 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 233 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 223 | 234 | </div> | |
| 224 | 235 | <div> | |
| 225 | 236 | <dt> | |
| @@ -199,14 +199,25 @@ | |||||
| 199 | <dt> | 199 | <dt> | ||
| 200 | <kbd>enter</kbd> | 200 | <kbd>enter</kbd> | ||
| 201 | </dt> | 201 | </dt> | ||
| 202 | - | <dd>open the row under the cursor</dd> | 202 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 203 | + | </div> | |||
| 204 | + | <div> | |||
| 205 | + | <dt> | |||
| 206 | + | <kbd>↑</kbd> | |||
| 207 | + | <kbd>↓</kbd> | |||
| 208 | + | <kbd>←</kbd> | |||
| 209 | + | <kbd>→</kbd> | |||
| 210 | + | <kbd>home</kbd> | |||
| 211 | + | <kbd>end</kbd> | |||
| 212 | + | </dt> | |||
| 213 | + | <dd>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</dd> | |||
| 203 | </div> | 214 | </div> | ||
| 204 | <div> | 215 | <div> | ||
| 205 | <dt> | 216 | <dt> | ||
| 206 | <kbd>l</kbd> | 217 | <kbd>l</kbd> | ||
| 207 | <kbd>h</kbd> | 218 | <kbd>h</kbd> | ||
| 208 | </dt> | 219 | </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> | 220 | + | <dd>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</dd> |
| 210 | </div> | 221 | </div> | ||
| 211 | <div> | 222 | <div> | ||
| 212 | <dt> | 223 | <dt> | ||
| @@ -219,7 +230,7 @@ | |||||
| 219 | <dt> | 230 | <dt> | ||
| 220 | <kbd>/</kbd> | 231 | <kbd>/</kbd> | ||
| 221 | </dt> | 232 | </dt> | ||
| 222 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 233 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 223 | </div> | 234 | </div> | ||
| 224 | <div> | 235 | <div> | ||
| 225 | <dt> | 236 | <dt> | ||
M internal/app/testdata/web/blob-text.html +14 -3
| @@ -194,14 +194,25 @@ | |||
| 194 | 194 | <dt> | |
| 195 | 195 | <kbd>enter</kbd> | |
| 196 | 196 | </dt> | |
| 197 | - | <dd>open the row under the cursor</dd> | |
| 197 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 198 | + | </div> | |
| 199 | + | <div> | |
| 200 | + | <dt> | |
| 201 | + | <kbd>↑</kbd> | |
| 202 | + | <kbd>↓</kbd> | |
| 203 | + | <kbd>←</kbd> | |
| 204 | + | <kbd>→</kbd> | |
| 205 | + | <kbd>home</kbd> | |
| 206 | + | <kbd>end</kbd> | |
| 207 | + | </dt> | |
| 208 | + | <dd>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</dd> | |
| 198 | 209 | </div> | |
| 199 | 210 | <div> | |
| 200 | 211 | <dt> | |
| 201 | 212 | <kbd>l</kbd> | |
| 202 | 213 | <kbd>h</kbd> | |
| 203 | 214 | </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> | |
| 215 | + | <dd>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</dd> | |
| 205 | 216 | </div> | |
| 206 | 217 | <div> | |
| 207 | 218 | <dt> | |
| @@ -214,7 +225,7 @@ | |||
| 214 | 225 | <dt> | |
| 215 | 226 | <kbd>/</kbd> | |
| 216 | 227 | </dt> | |
| 217 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 228 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 218 | 229 | </div> | |
| 219 | 230 | <div> | |
| 220 | 231 | <dt> | |
| @@ -194,14 +194,25 @@ | |||||
| 194 | <dt> | 194 | <dt> | ||
| 195 | <kbd>enter</kbd> | 195 | <kbd>enter</kbd> | ||
| 196 | </dt> | 196 | </dt> | ||
| 197 | - | <dd>open the row under the cursor</dd> | 197 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 198 | + | </div> | |||
| 199 | + | <div> | |||
| 200 | + | <dt> | |||
| 201 | + | <kbd>↑</kbd> | |||
| 202 | + | <kbd>↓</kbd> | |||
| 203 | + | <kbd>←</kbd> | |||
| 204 | + | <kbd>→</kbd> | |||
| 205 | + | <kbd>home</kbd> | |||
| 206 | + | <kbd>end</kbd> | |||
| 207 | + | </dt> | |||
| 208 | + | <dd>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</dd> | |||
| 198 | </div> | 209 | </div> | ||
| 199 | <div> | 210 | <div> | ||
| 200 | <dt> | 211 | <dt> | ||
| 201 | <kbd>l</kbd> | 212 | <kbd>l</kbd> | ||
| 202 | <kbd>h</kbd> | 213 | <kbd>h</kbd> | ||
| 203 | </dt> | 214 | </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> | 215 | + | <dd>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</dd> |
| 205 | </div> | 216 | </div> | ||
| 206 | <div> | 217 | <div> | ||
| 207 | <dt> | 218 | <dt> | ||
| @@ -214,7 +225,7 @@ | |||||
| 214 | <dt> | 225 | <dt> | ||
| 215 | <kbd>/</kbd> | 226 | <kbd>/</kbd> | ||
| 216 | </dt> | 227 | </dt> | ||
| 217 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 228 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 218 | </div> | 229 | </div> | ||
| 219 | <div> | 230 | <div> | ||
| 220 | <dt> | 231 | <dt> | ||
M internal/app/testdata/web/blob-too-large.html +14 -3
| @@ -174,14 +174,25 @@ | |||
| 174 | 174 | <dt> | |
| 175 | 175 | <kbd>enter</kbd> | |
| 176 | 176 | </dt> | |
| 177 | - | <dd>open the row under the cursor</dd> | |
| 177 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 178 | + | </div> | |
| 179 | + | <div> | |
| 180 | + | <dt> | |
| 181 | + | <kbd>↑</kbd> | |
| 182 | + | <kbd>↓</kbd> | |
| 183 | + | <kbd>←</kbd> | |
| 184 | + | <kbd>→</kbd> | |
| 185 | + | <kbd>home</kbd> | |
| 186 | + | <kbd>end</kbd> | |
| 187 | + | </dt> | |
| 188 | + | <dd>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</dd> | |
| 178 | 189 | </div> | |
| 179 | 190 | <div> | |
| 180 | 191 | <dt> | |
| 181 | 192 | <kbd>l</kbd> | |
| 182 | 193 | <kbd>h</kbd> | |
| 183 | 194 | </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> | |
| 195 | + | <dd>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</dd> | |
| 185 | 196 | </div> | |
| 186 | 197 | <div> | |
| 187 | 198 | <dt> | |
| @@ -194,7 +205,7 @@ | |||
| 194 | 205 | <dt> | |
| 195 | 206 | <kbd>/</kbd> | |
| 196 | 207 | </dt> | |
| 197 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 208 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 198 | 209 | </div> | |
| 199 | 210 | <div> | |
| 200 | 211 | <dt> | |
| @@ -174,14 +174,25 @@ | |||||
| 174 | <dt> | 174 | <dt> | ||
| 175 | <kbd>enter</kbd> | 175 | <kbd>enter</kbd> | ||
| 176 | </dt> | 176 | </dt> | ||
| 177 | - | <dd>open the row under the cursor</dd> | 177 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 178 | + | </div> | |||
| 179 | + | <div> | |||
| 180 | + | <dt> | |||
| 181 | + | <kbd>↑</kbd> | |||
| 182 | + | <kbd>↓</kbd> | |||
| 183 | + | <kbd>←</kbd> | |||
| 184 | + | <kbd>→</kbd> | |||
| 185 | + | <kbd>home</kbd> | |||
| 186 | + | <kbd>end</kbd> | |||
| 187 | + | </dt> | |||
| 188 | + | <dd>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</dd> | |||
| 178 | </div> | 189 | </div> | ||
| 179 | <div> | 190 | <div> | ||
| 180 | <dt> | 191 | <dt> | ||
| 181 | <kbd>l</kbd> | 192 | <kbd>l</kbd> | ||
| 182 | <kbd>h</kbd> | 193 | <kbd>h</kbd> | ||
| 183 | </dt> | 194 | </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> | 195 | + | <dd>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</dd> |
| 185 | </div> | 196 | </div> | ||
| 186 | <div> | 197 | <div> | ||
| 187 | <dt> | 198 | <dt> | ||
| @@ -194,7 +205,7 @@ | |||||
| 194 | <dt> | 205 | <dt> | ||
| 195 | <kbd>/</kbd> | 206 | <kbd>/</kbd> | ||
| 196 | </dt> | 207 | </dt> | ||
| 197 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 208 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 198 | </div> | 209 | </div> | ||
| 199 | <div> | 210 | <div> | ||
| 200 | <dt> | 211 | <dt> | ||
M internal/app/testdata/web/blob-trojan.html +14 -3
| @@ -241,14 +241,25 @@ | |||
| 241 | 241 | <dt> | |
| 242 | 242 | <kbd>enter</kbd> | |
| 243 | 243 | </dt> | |
| 244 | - | <dd>open the row under the cursor</dd> | |
| 244 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 245 | + | </div> | |
| 246 | + | <div> | |
| 247 | + | <dt> | |
| 248 | + | <kbd>↑</kbd> | |
| 249 | + | <kbd>↓</kbd> | |
| 250 | + | <kbd>←</kbd> | |
| 251 | + | <kbd>→</kbd> | |
| 252 | + | <kbd>home</kbd> | |
| 253 | + | <kbd>end</kbd> | |
| 254 | + | </dt> | |
| 255 | + | <dd>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</dd> | |
| 245 | 256 | </div> | |
| 246 | 257 | <div> | |
| 247 | 258 | <dt> | |
| 248 | 259 | <kbd>l</kbd> | |
| 249 | 260 | <kbd>h</kbd> | |
| 250 | 261 | </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> | |
| 262 | + | <dd>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</dd> | |
| 252 | 263 | </div> | |
| 253 | 264 | <div> | |
| 254 | 265 | <dt> | |
| @@ -261,7 +272,7 @@ | |||
| 261 | 272 | <dt> | |
| 262 | 273 | <kbd>/</kbd> | |
| 263 | 274 | </dt> | |
| 264 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 275 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 265 | 276 | </div> | |
| 266 | 277 | <div> | |
| 267 | 278 | <dt> | |
| @@ -241,14 +241,25 @@ | |||||
| 241 | <dt> | 241 | <dt> | ||
| 242 | <kbd>enter</kbd> | 242 | <kbd>enter</kbd> | ||
| 243 | </dt> | 243 | </dt> | ||
| 244 | - | <dd>open the row under the cursor</dd> | 244 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 245 | + | </div> | |||
| 246 | + | <div> | |||
| 247 | + | <dt> | |||
| 248 | + | <kbd>↑</kbd> | |||
| 249 | + | <kbd>↓</kbd> | |||
| 250 | + | <kbd>←</kbd> | |||
| 251 | + | <kbd>→</kbd> | |||
| 252 | + | <kbd>home</kbd> | |||
| 253 | + | <kbd>end</kbd> | |||
| 254 | + | </dt> | |||
| 255 | + | <dd>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</dd> | |||
| 245 | </div> | 256 | </div> | ||
| 246 | <div> | 257 | <div> | ||
| 247 | <dt> | 258 | <dt> | ||
| 248 | <kbd>l</kbd> | 259 | <kbd>l</kbd> | ||
| 249 | <kbd>h</kbd> | 260 | <kbd>h</kbd> | ||
| 250 | </dt> | 261 | </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> | 262 | + | <dd>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</dd> |
| 252 | </div> | 263 | </div> | ||
| 253 | <div> | 264 | <div> | ||
| 254 | <dt> | 265 | <dt> | ||
| @@ -261,7 +272,7 @@ | |||||
| 261 | <dt> | 272 | <dt> | ||
| 262 | <kbd>/</kbd> | 273 | <kbd>/</kbd> | ||
| 263 | </dt> | 274 | </dt> | ||
| 264 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 275 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 265 | </div> | 276 | </div> | ||
| 266 | <div> | 277 | <div> | ||
| 267 | <dt> | 278 | <dt> | ||
M internal/app/testdata/web/commit-big.html +14 -3
| @@ -212,14 +212,25 @@ | |||
| 212 | 212 | <dt> | |
| 213 | 213 | <kbd>enter</kbd> | |
| 214 | 214 | </dt> | |
| 215 | - | <dd>open the row under the cursor</dd> | |
| 215 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 216 | + | </div> | |
| 217 | + | <div> | |
| 218 | + | <dt> | |
| 219 | + | <kbd>↑</kbd> | |
| 220 | + | <kbd>↓</kbd> | |
| 221 | + | <kbd>←</kbd> | |
| 222 | + | <kbd>→</kbd> | |
| 223 | + | <kbd>home</kbd> | |
| 224 | + | <kbd>end</kbd> | |
| 225 | + | </dt> | |
| 226 | + | <dd>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</dd> | |
| 216 | 227 | </div> | |
| 217 | 228 | <div> | |
| 218 | 229 | <dt> | |
| 219 | 230 | <kbd>l</kbd> | |
| 220 | 231 | <kbd>h</kbd> | |
| 221 | 232 | </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> | |
| 233 | + | <dd>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</dd> | |
| 223 | 234 | </div> | |
| 224 | 235 | <div> | |
| 225 | 236 | <dt> | |
| @@ -232,7 +243,7 @@ | |||
| 232 | 243 | <dt> | |
| 233 | 244 | <kbd>/</kbd> | |
| 234 | 245 | </dt> | |
| 235 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 246 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 236 | 247 | </div> | |
| 237 | 248 | <div> | |
| 238 | 249 | <dt> | |
| @@ -212,14 +212,25 @@ | |||||
| 212 | <dt> | 212 | <dt> | ||
| 213 | <kbd>enter</kbd> | 213 | <kbd>enter</kbd> | ||
| 214 | </dt> | 214 | </dt> | ||
| 215 | - | <dd>open the row under the cursor</dd> | 215 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 216 | + | </div> | |||
| 217 | + | <div> | |||
| 218 | + | <dt> | |||
| 219 | + | <kbd>↑</kbd> | |||
| 220 | + | <kbd>↓</kbd> | |||
| 221 | + | <kbd>←</kbd> | |||
| 222 | + | <kbd>→</kbd> | |||
| 223 | + | <kbd>home</kbd> | |||
| 224 | + | <kbd>end</kbd> | |||
| 225 | + | </dt> | |||
| 226 | + | <dd>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</dd> | |||
| 216 | </div> | 227 | </div> | ||
| 217 | <div> | 228 | <div> | ||
| 218 | <dt> | 229 | <dt> | ||
| 219 | <kbd>l</kbd> | 230 | <kbd>l</kbd> | ||
| 220 | <kbd>h</kbd> | 231 | <kbd>h</kbd> | ||
| 221 | </dt> | 232 | </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> | 233 | + | <dd>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</dd> |
| 223 | </div> | 234 | </div> | ||
| 224 | <div> | 235 | <div> | ||
| 225 | <dt> | 236 | <dt> | ||
| @@ -232,7 +243,7 @@ | |||||
| 232 | <dt> | 243 | <dt> | ||
| 233 | <kbd>/</kbd> | 244 | <kbd>/</kbd> | ||
| 234 | </dt> | 245 | </dt> | ||
| 235 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 246 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 236 | </div> | 247 | </div> | ||
| 237 | <div> | 248 | <div> | ||
| 238 | <dt> | 249 | <dt> | ||
M internal/app/testdata/web/commit-binary.html +14 -3
| @@ -204,14 +204,25 @@ | |||
| 204 | 204 | <dt> | |
| 205 | 205 | <kbd>enter</kbd> | |
| 206 | 206 | </dt> | |
| 207 | - | <dd>open the row under the cursor</dd> | |
| 207 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 208 | + | </div> | |
| 209 | + | <div> | |
| 210 | + | <dt> | |
| 211 | + | <kbd>↑</kbd> | |
| 212 | + | <kbd>↓</kbd> | |
| 213 | + | <kbd>←</kbd> | |
| 214 | + | <kbd>→</kbd> | |
| 215 | + | <kbd>home</kbd> | |
| 216 | + | <kbd>end</kbd> | |
| 217 | + | </dt> | |
| 218 | + | <dd>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</dd> | |
| 208 | 219 | </div> | |
| 209 | 220 | <div> | |
| 210 | 221 | <dt> | |
| 211 | 222 | <kbd>l</kbd> | |
| 212 | 223 | <kbd>h</kbd> | |
| 213 | 224 | </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> | |
| 225 | + | <dd>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</dd> | |
| 215 | 226 | </div> | |
| 216 | 227 | <div> | |
| 217 | 228 | <dt> | |
| @@ -224,7 +235,7 @@ | |||
| 224 | 235 | <dt> | |
| 225 | 236 | <kbd>/</kbd> | |
| 226 | 237 | </dt> | |
| 227 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 238 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 228 | 239 | </div> | |
| 229 | 240 | <div> | |
| 230 | 241 | <dt> | |
| @@ -204,14 +204,25 @@ | |||||
| 204 | <dt> | 204 | <dt> | ||
| 205 | <kbd>enter</kbd> | 205 | <kbd>enter</kbd> | ||
| 206 | </dt> | 206 | </dt> | ||
| 207 | - | <dd>open the row under the cursor</dd> | 207 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 208 | + | </div> | |||
| 209 | + | <div> | |||
| 210 | + | <dt> | |||
| 211 | + | <kbd>↑</kbd> | |||
| 212 | + | <kbd>↓</kbd> | |||
| 213 | + | <kbd>←</kbd> | |||
| 214 | + | <kbd>→</kbd> | |||
| 215 | + | <kbd>home</kbd> | |||
| 216 | + | <kbd>end</kbd> | |||
| 217 | + | </dt> | |||
| 218 | + | <dd>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</dd> | |||
| 208 | </div> | 219 | </div> | ||
| 209 | <div> | 220 | <div> | ||
| 210 | <dt> | 221 | <dt> | ||
| 211 | <kbd>l</kbd> | 222 | <kbd>l</kbd> | ||
| 212 | <kbd>h</kbd> | 223 | <kbd>h</kbd> | ||
| 213 | </dt> | 224 | </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> | 225 | + | <dd>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</dd> |
| 215 | </div> | 226 | </div> | ||
| 216 | <div> | 227 | <div> | ||
| 217 | <dt> | 228 | <dt> | ||
| @@ -224,7 +235,7 @@ | |||||
| 224 | <dt> | 235 | <dt> | ||
| 225 | <kbd>/</kbd> | 236 | <kbd>/</kbd> | ||
| 226 | </dt> | 237 | </dt> | ||
| 227 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 238 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 228 | </div> | 239 | </div> | ||
| 229 | <div> | 240 | <div> | ||
| 230 | <dt> | 241 | <dt> | ||
M internal/app/testdata/web/commit-merge.html +14 -3
| @@ -246,14 +246,25 @@ | |||
| 246 | 246 | <dt> | |
| 247 | 247 | <kbd>enter</kbd> | |
| 248 | 248 | </dt> | |
| 249 | - | <dd>open the row under the cursor</dd> | |
| 249 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 250 | + | </div> | |
| 251 | + | <div> | |
| 252 | + | <dt> | |
| 253 | + | <kbd>↑</kbd> | |
| 254 | + | <kbd>↓</kbd> | |
| 255 | + | <kbd>←</kbd> | |
| 256 | + | <kbd>→</kbd> | |
| 257 | + | <kbd>home</kbd> | |
| 258 | + | <kbd>end</kbd> | |
| 259 | + | </dt> | |
| 260 | + | <dd>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</dd> | |
| 250 | 261 | </div> | |
| 251 | 262 | <div> | |
| 252 | 263 | <dt> | |
| 253 | 264 | <kbd>l</kbd> | |
| 254 | 265 | <kbd>h</kbd> | |
| 255 | 266 | </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> | |
| 267 | + | <dd>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</dd> | |
| 257 | 268 | </div> | |
| 258 | 269 | <div> | |
| 259 | 270 | <dt> | |
| @@ -266,7 +277,7 @@ | |||
| 266 | 277 | <dt> | |
| 267 | 278 | <kbd>/</kbd> | |
| 268 | 279 | </dt> | |
| 269 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 280 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 270 | 281 | </div> | |
| 271 | 282 | <div> | |
| 272 | 283 | <dt> | |
| @@ -246,14 +246,25 @@ | |||||
| 246 | <dt> | 246 | <dt> | ||
| 247 | <kbd>enter</kbd> | 247 | <kbd>enter</kbd> | ||
| 248 | </dt> | 248 | </dt> | ||
| 249 | - | <dd>open the row under the cursor</dd> | 249 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 250 | + | </div> | |||
| 251 | + | <div> | |||
| 252 | + | <dt> | |||
| 253 | + | <kbd>↑</kbd> | |||
| 254 | + | <kbd>↓</kbd> | |||
| 255 | + | <kbd>←</kbd> | |||
| 256 | + | <kbd>→</kbd> | |||
| 257 | + | <kbd>home</kbd> | |||
| 258 | + | <kbd>end</kbd> | |||
| 259 | + | </dt> | |||
| 260 | + | <dd>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</dd> | |||
| 250 | </div> | 261 | </div> | ||
| 251 | <div> | 262 | <div> | ||
| 252 | <dt> | 263 | <dt> | ||
| 253 | <kbd>l</kbd> | 264 | <kbd>l</kbd> | ||
| 254 | <kbd>h</kbd> | 265 | <kbd>h</kbd> | ||
| 255 | </dt> | 266 | </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> | 267 | + | <dd>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</dd> |
| 257 | </div> | 268 | </div> | ||
| 258 | <div> | 269 | <div> | ||
| 259 | <dt> | 270 | <dt> | ||
| @@ -266,7 +277,7 @@ | |||||
| 266 | <dt> | 277 | <dt> | ||
| 267 | <kbd>/</kbd> | 278 | <kbd>/</kbd> | ||
| 268 | </dt> | 279 | </dt> | ||
| 269 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 280 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 270 | </div> | 281 | </div> | ||
| 271 | <div> | 282 | <div> | ||
| 272 | <dt> | 283 | <dt> | ||
M internal/app/testdata/web/commit-modify-split.html +14 -3
| @@ -369,14 +369,25 @@ And <b>html</b> & "quotes".</pre> | |||
| 369 | 369 | <dt> | |
| 370 | 370 | <kbd>enter</kbd> | |
| 371 | 371 | </dt> | |
| 372 | - | <dd>open the row under the cursor</dd> | |
| 372 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 373 | + | </div> | |
| 374 | + | <div> | |
| 375 | + | <dt> | |
| 376 | + | <kbd>↑</kbd> | |
| 377 | + | <kbd>↓</kbd> | |
| 378 | + | <kbd>←</kbd> | |
| 379 | + | <kbd>→</kbd> | |
| 380 | + | <kbd>home</kbd> | |
| 381 | + | <kbd>end</kbd> | |
| 382 | + | </dt> | |
| 383 | + | <dd>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</dd> | |
| 373 | 384 | </div> | |
| 374 | 385 | <div> | |
| 375 | 386 | <dt> | |
| 376 | 387 | <kbd>l</kbd> | |
| 377 | 388 | <kbd>h</kbd> | |
| 378 | 389 | </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> | |
| 390 | + | <dd>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</dd> | |
| 380 | 391 | </div> | |
| 381 | 392 | <div> | |
| 382 | 393 | <dt> | |
| @@ -389,7 +400,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 389 | 400 | <dt> | |
| 390 | 401 | <kbd>/</kbd> | |
| 391 | 402 | </dt> | |
| 392 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 403 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 393 | 404 | </div> | |
| 394 | 405 | <div> | |
| 395 | 406 | <dt> | |
| @@ -369,14 +369,25 @@ And <b>html</b> & "quotes".</pre> | |||||
| 369 | <dt> | 369 | <dt> | ||
| 370 | <kbd>enter</kbd> | 370 | <kbd>enter</kbd> | ||
| 371 | </dt> | 371 | </dt> | ||
| 372 | - | <dd>open the row under the cursor</dd> | 372 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 373 | + | </div> | |||
| 374 | + | <div> | |||
| 375 | + | <dt> | |||
| 376 | + | <kbd>↑</kbd> | |||
| 377 | + | <kbd>↓</kbd> | |||
| 378 | + | <kbd>←</kbd> | |||
| 379 | + | <kbd>→</kbd> | |||
| 380 | + | <kbd>home</kbd> | |||
| 381 | + | <kbd>end</kbd> | |||
| 382 | + | </dt> | |||
| 383 | + | <dd>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</dd> | |||
| 373 | </div> | 384 | </div> | ||
| 374 | <div> | 385 | <div> | ||
| 375 | <dt> | 386 | <dt> | ||
| 376 | <kbd>l</kbd> | 387 | <kbd>l</kbd> | ||
| 377 | <kbd>h</kbd> | 388 | <kbd>h</kbd> | ||
| 378 | </dt> | 389 | </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> | 390 | + | <dd>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</dd> |
| 380 | </div> | 391 | </div> | ||
| 381 | <div> | 392 | <div> | ||
| 382 | <dt> | 393 | <dt> | ||
| @@ -389,7 +400,7 @@ And <b>html</b> & "quotes".</pre> | |||||
| 389 | <dt> | 400 | <dt> | ||
| 390 | <kbd>/</kbd> | 401 | <kbd>/</kbd> | ||
| 391 | </dt> | 402 | </dt> | ||
| 392 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 403 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 393 | </div> | 404 | </div> | ||
| 394 | <div> | 405 | <div> | ||
| 395 | <dt> | 406 | <dt> | ||
M internal/app/testdata/web/commit-modify.html +14 -3
| @@ -369,14 +369,25 @@ And <b>html</b> & "quotes".</pre> | |||
| 369 | 369 | <dt> | |
| 370 | 370 | <kbd>enter</kbd> | |
| 371 | 371 | </dt> | |
| 372 | - | <dd>open the row under the cursor</dd> | |
| 372 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 373 | + | </div> | |
| 374 | + | <div> | |
| 375 | + | <dt> | |
| 376 | + | <kbd>↑</kbd> | |
| 377 | + | <kbd>↓</kbd> | |
| 378 | + | <kbd>←</kbd> | |
| 379 | + | <kbd>→</kbd> | |
| 380 | + | <kbd>home</kbd> | |
| 381 | + | <kbd>end</kbd> | |
| 382 | + | </dt> | |
| 383 | + | <dd>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</dd> | |
| 373 | 384 | </div> | |
| 374 | 385 | <div> | |
| 375 | 386 | <dt> | |
| 376 | 387 | <kbd>l</kbd> | |
| 377 | 388 | <kbd>h</kbd> | |
| 378 | 389 | </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> | |
| 390 | + | <dd>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</dd> | |
| 380 | 391 | </div> | |
| 381 | 392 | <div> | |
| 382 | 393 | <dt> | |
| @@ -389,7 +400,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 389 | 400 | <dt> | |
| 390 | 401 | <kbd>/</kbd> | |
| 391 | 402 | </dt> | |
| 392 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 403 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 393 | 404 | </div> | |
| 394 | 405 | <div> | |
| 395 | 406 | <dt> | |
| @@ -369,14 +369,25 @@ And <b>html</b> & "quotes".</pre> | |||||
| 369 | <dt> | 369 | <dt> | ||
| 370 | <kbd>enter</kbd> | 370 | <kbd>enter</kbd> | ||
| 371 | </dt> | 371 | </dt> | ||
| 372 | - | <dd>open the row under the cursor</dd> | 372 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 373 | + | </div> | |||
| 374 | + | <div> | |||
| 375 | + | <dt> | |||
| 376 | + | <kbd>↑</kbd> | |||
| 377 | + | <kbd>↓</kbd> | |||
| 378 | + | <kbd>←</kbd> | |||
| 379 | + | <kbd>→</kbd> | |||
| 380 | + | <kbd>home</kbd> | |||
| 381 | + | <kbd>end</kbd> | |||
| 382 | + | </dt> | |||
| 383 | + | <dd>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</dd> | |||
| 373 | </div> | 384 | </div> | ||
| 374 | <div> | 385 | <div> | ||
| 375 | <dt> | 386 | <dt> | ||
| 376 | <kbd>l</kbd> | 387 | <kbd>l</kbd> | ||
| 377 | <kbd>h</kbd> | 388 | <kbd>h</kbd> | ||
| 378 | </dt> | 389 | </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> | 390 | + | <dd>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</dd> |
| 380 | </div> | 391 | </div> | ||
| 381 | <div> | 392 | <div> | ||
| 382 | <dt> | 393 | <dt> | ||
| @@ -389,7 +400,7 @@ And <b>html</b> & "quotes".</pre> | |||||
| 389 | <dt> | 400 | <dt> | ||
| 390 | <kbd>/</kbd> | 401 | <kbd>/</kbd> | ||
| 391 | </dt> | 402 | </dt> | ||
| 392 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 403 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 393 | </div> | 404 | </div> | ||
| 394 | <div> | 405 | <div> | ||
| 395 | <dt> | 406 | <dt> | ||
M internal/app/testdata/web/commit-rename.html +14 -3
| @@ -273,14 +273,25 @@ | |||
| 273 | 273 | <dt> | |
| 274 | 274 | <kbd>enter</kbd> | |
| 275 | 275 | </dt> | |
| 276 | - | <dd>open the row under the cursor</dd> | |
| 276 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 277 | + | </div> | |
| 278 | + | <div> | |
| 279 | + | <dt> | |
| 280 | + | <kbd>↑</kbd> | |
| 281 | + | <kbd>↓</kbd> | |
| 282 | + | <kbd>←</kbd> | |
| 283 | + | <kbd>→</kbd> | |
| 284 | + | <kbd>home</kbd> | |
| 285 | + | <kbd>end</kbd> | |
| 286 | + | </dt> | |
| 287 | + | <dd>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</dd> | |
| 277 | 288 | </div> | |
| 278 | 289 | <div> | |
| 279 | 290 | <dt> | |
| 280 | 291 | <kbd>l</kbd> | |
| 281 | 292 | <kbd>h</kbd> | |
| 282 | 293 | </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> | |
| 294 | + | <dd>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</dd> | |
| 284 | 295 | </div> | |
| 285 | 296 | <div> | |
| 286 | 297 | <dt> | |
| @@ -293,7 +304,7 @@ | |||
| 293 | 304 | <dt> | |
| 294 | 305 | <kbd>/</kbd> | |
| 295 | 306 | </dt> | |
| 296 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 307 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 297 | 308 | </div> | |
| 298 | 309 | <div> | |
| 299 | 310 | <dt> | |
| @@ -273,14 +273,25 @@ | |||||
| 273 | <dt> | 273 | <dt> | ||
| 274 | <kbd>enter</kbd> | 274 | <kbd>enter</kbd> | ||
| 275 | </dt> | 275 | </dt> | ||
| 276 | - | <dd>open the row under the cursor</dd> | 276 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 277 | + | </div> | |||
| 278 | + | <div> | |||
| 279 | + | <dt> | |||
| 280 | + | <kbd>↑</kbd> | |||
| 281 | + | <kbd>↓</kbd> | |||
| 282 | + | <kbd>←</kbd> | |||
| 283 | + | <kbd>→</kbd> | |||
| 284 | + | <kbd>home</kbd> | |||
| 285 | + | <kbd>end</kbd> | |||
| 286 | + | </dt> | |||
| 287 | + | <dd>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</dd> | |||
| 277 | </div> | 288 | </div> | ||
| 278 | <div> | 289 | <div> | ||
| 279 | <dt> | 290 | <dt> | ||
| 280 | <kbd>l</kbd> | 291 | <kbd>l</kbd> | ||
| 281 | <kbd>h</kbd> | 292 | <kbd>h</kbd> | ||
| 282 | </dt> | 293 | </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> | 294 | + | <dd>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</dd> |
| 284 | </div> | 295 | </div> | ||
| 285 | <div> | 296 | <div> | ||
| 286 | <dt> | 297 | <dt> | ||
| @@ -293,7 +304,7 @@ | |||||
| 293 | <dt> | 304 | <dt> | ||
| 294 | <kbd>/</kbd> | 305 | <kbd>/</kbd> | ||
| 295 | </dt> | 306 | </dt> | ||
| 296 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 307 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 297 | </div> | 308 | </div> | ||
| 298 | <div> | 309 | <div> | ||
| 299 | <dt> | 310 | <dt> | ||
M internal/app/testdata/web/commit-root.html +14 -3
| @@ -592,14 +592,25 @@ | |||
| 592 | 592 | <dt> | |
| 593 | 593 | <kbd>enter</kbd> | |
| 594 | 594 | </dt> | |
| 595 | - | <dd>open the row under the cursor</dd> | |
| 595 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 596 | + | </div> | |
| 597 | + | <div> | |
| 598 | + | <dt> | |
| 599 | + | <kbd>↑</kbd> | |
| 600 | + | <kbd>↓</kbd> | |
| 601 | + | <kbd>←</kbd> | |
| 602 | + | <kbd>→</kbd> | |
| 603 | + | <kbd>home</kbd> | |
| 604 | + | <kbd>end</kbd> | |
| 605 | + | </dt> | |
| 606 | + | <dd>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</dd> | |
| 596 | 607 | </div> | |
| 597 | 608 | <div> | |
| 598 | 609 | <dt> | |
| 599 | 610 | <kbd>l</kbd> | |
| 600 | 611 | <kbd>h</kbd> | |
| 601 | 612 | </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> | |
| 613 | + | <dd>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</dd> | |
| 603 | 614 | </div> | |
| 604 | 615 | <div> | |
| 605 | 616 | <dt> | |
| @@ -612,7 +623,7 @@ | |||
| 612 | 623 | <dt> | |
| 613 | 624 | <kbd>/</kbd> | |
| 614 | 625 | </dt> | |
| 615 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 626 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 616 | 627 | </div> | |
| 617 | 628 | <div> | |
| 618 | 629 | <dt> | |
| @@ -592,14 +592,25 @@ | |||||
| 592 | <dt> | 592 | <dt> | ||
| 593 | <kbd>enter</kbd> | 593 | <kbd>enter</kbd> | ||
| 594 | </dt> | 594 | </dt> | ||
| 595 | - | <dd>open the row under the cursor</dd> | 595 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 596 | + | </div> | |||
| 597 | + | <div> | |||
| 598 | + | <dt> | |||
| 599 | + | <kbd>↑</kbd> | |||
| 600 | + | <kbd>↓</kbd> | |||
| 601 | + | <kbd>←</kbd> | |||
| 602 | + | <kbd>→</kbd> | |||
| 603 | + | <kbd>home</kbd> | |||
| 604 | + | <kbd>end</kbd> | |||
| 605 | + | </dt> | |||
| 606 | + | <dd>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</dd> | |||
| 596 | </div> | 607 | </div> | ||
| 597 | <div> | 608 | <div> | ||
| 598 | <dt> | 609 | <dt> | ||
| 599 | <kbd>l</kbd> | 610 | <kbd>l</kbd> | ||
| 600 | <kbd>h</kbd> | 611 | <kbd>h</kbd> | ||
| 601 | </dt> | 612 | </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> | 613 | + | <dd>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</dd> |
| 603 | </div> | 614 | </div> | ||
| 604 | <div> | 615 | <div> | ||
| 605 | <dt> | 616 | <dt> | ||
| @@ -612,7 +623,7 @@ | |||||
| 612 | <dt> | 623 | <dt> | ||
| 613 | <kbd>/</kbd> | 624 | <kbd>/</kbd> | ||
| 614 | </dt> | 625 | </dt> | ||
| 615 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 626 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 616 | </div> | 627 | </div> | ||
| 617 | <div> | 628 | <div> | ||
| 618 | <dt> | 629 | <dt> | ||
M internal/app/testdata/web/commit-trojan.html +14 -3
| @@ -381,14 +381,25 @@ | |||
| 381 | 381 | <dt> | |
| 382 | 382 | <kbd>enter</kbd> | |
| 383 | 383 | </dt> | |
| 384 | - | <dd>open the row under the cursor</dd> | |
| 384 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 385 | + | </div> | |
| 386 | + | <div> | |
| 387 | + | <dt> | |
| 388 | + | <kbd>↑</kbd> | |
| 389 | + | <kbd>↓</kbd> | |
| 390 | + | <kbd>←</kbd> | |
| 391 | + | <kbd>→</kbd> | |
| 392 | + | <kbd>home</kbd> | |
| 393 | + | <kbd>end</kbd> | |
| 394 | + | </dt> | |
| 395 | + | <dd>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</dd> | |
| 385 | 396 | </div> | |
| 386 | 397 | <div> | |
| 387 | 398 | <dt> | |
| 388 | 399 | <kbd>l</kbd> | |
| 389 | 400 | <kbd>h</kbd> | |
| 390 | 401 | </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> | |
| 402 | + | <dd>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</dd> | |
| 392 | 403 | </div> | |
| 393 | 404 | <div> | |
| 394 | 405 | <dt> | |
| @@ -401,7 +412,7 @@ | |||
| 401 | 412 | <dt> | |
| 402 | 413 | <kbd>/</kbd> | |
| 403 | 414 | </dt> | |
| 404 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 415 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 405 | 416 | </div> | |
| 406 | 417 | <div> | |
| 407 | 418 | <dt> | |
| @@ -381,14 +381,25 @@ | |||||
| 381 | <dt> | 381 | <dt> | ||
| 382 | <kbd>enter</kbd> | 382 | <kbd>enter</kbd> | ||
| 383 | </dt> | 383 | </dt> | ||
| 384 | - | <dd>open the row under the cursor</dd> | 384 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 385 | + | </div> | |||
| 386 | + | <div> | |||
| 387 | + | <dt> | |||
| 388 | + | <kbd>↑</kbd> | |||
| 389 | + | <kbd>↓</kbd> | |||
| 390 | + | <kbd>←</kbd> | |||
| 391 | + | <kbd>→</kbd> | |||
| 392 | + | <kbd>home</kbd> | |||
| 393 | + | <kbd>end</kbd> | |||
| 394 | + | </dt> | |||
| 395 | + | <dd>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</dd> | |||
| 385 | </div> | 396 | </div> | ||
| 386 | <div> | 397 | <div> | ||
| 387 | <dt> | 398 | <dt> | ||
| 388 | <kbd>l</kbd> | 399 | <kbd>l</kbd> | ||
| 389 | <kbd>h</kbd> | 400 | <kbd>h</kbd> | ||
| 390 | </dt> | 401 | </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> | 402 | + | <dd>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</dd> |
| 392 | </div> | 403 | </div> | ||
| 393 | <div> | 404 | <div> | ||
| 394 | <dt> | 405 | <dt> | ||
| @@ -401,7 +412,7 @@ | |||||
| 401 | <dt> | 412 | <dt> | ||
| 402 | <kbd>/</kbd> | 413 | <kbd>/</kbd> | ||
| 403 | </dt> | 414 | </dt> | ||
| 404 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 415 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 405 | </div> | 416 | </div> | ||
| 406 | <div> | 417 | <div> | ||
| 407 | <dt> | 418 | <dt> | ||
M internal/app/testdata/web/commits-page2.html +14 -3
| @@ -205,14 +205,25 @@ | |||
| 205 | 205 | <dt> | |
| 206 | 206 | <kbd>enter</kbd> | |
| 207 | 207 | </dt> | |
| 208 | - | <dd>open the row under the cursor</dd> | |
| 208 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 209 | + | </div> | |
| 210 | + | <div> | |
| 211 | + | <dt> | |
| 212 | + | <kbd>↑</kbd> | |
| 213 | + | <kbd>↓</kbd> | |
| 214 | + | <kbd>←</kbd> | |
| 215 | + | <kbd>→</kbd> | |
| 216 | + | <kbd>home</kbd> | |
| 217 | + | <kbd>end</kbd> | |
| 218 | + | </dt> | |
| 219 | + | <dd>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</dd> | |
| 209 | 220 | </div> | |
| 210 | 221 | <div> | |
| 211 | 222 | <dt> | |
| 212 | 223 | <kbd>l</kbd> | |
| 213 | 224 | <kbd>h</kbd> | |
| 214 | 225 | </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> | |
| 226 | + | <dd>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</dd> | |
| 216 | 227 | </div> | |
| 217 | 228 | <div> | |
| 218 | 229 | <dt> | |
| @@ -225,7 +236,7 @@ | |||
| 225 | 236 | <dt> | |
| 226 | 237 | <kbd>/</kbd> | |
| 227 | 238 | </dt> | |
| 228 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 239 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 229 | 240 | </div> | |
| 230 | 241 | <div> | |
| 231 | 242 | <dt> | |
| @@ -205,14 +205,25 @@ | |||||
| 205 | <dt> | 205 | <dt> | ||
| 206 | <kbd>enter</kbd> | 206 | <kbd>enter</kbd> | ||
| 207 | </dt> | 207 | </dt> | ||
| 208 | - | <dd>open the row under the cursor</dd> | 208 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 209 | + | </div> | |||
| 210 | + | <div> | |||
| 211 | + | <dt> | |||
| 212 | + | <kbd>↑</kbd> | |||
| 213 | + | <kbd>↓</kbd> | |||
| 214 | + | <kbd>←</kbd> | |||
| 215 | + | <kbd>→</kbd> | |||
| 216 | + | <kbd>home</kbd> | |||
| 217 | + | <kbd>end</kbd> | |||
| 218 | + | </dt> | |||
| 219 | + | <dd>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</dd> | |||
| 209 | </div> | 220 | </div> | ||
| 210 | <div> | 221 | <div> | ||
| 211 | <dt> | 222 | <dt> | ||
| 212 | <kbd>l</kbd> | 223 | <kbd>l</kbd> | ||
| 213 | <kbd>h</kbd> | 224 | <kbd>h</kbd> | ||
| 214 | </dt> | 225 | </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> | 226 | + | <dd>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</dd> |
| 216 | </div> | 227 | </div> | ||
| 217 | <div> | 228 | <div> | ||
| 218 | <dt> | 229 | <dt> | ||
| @@ -225,7 +236,7 @@ | |||||
| 225 | <dt> | 236 | <dt> | ||
| 226 | <kbd>/</kbd> | 237 | <kbd>/</kbd> | ||
| 227 | </dt> | 238 | </dt> | ||
| 228 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 239 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 229 | </div> | 240 | </div> | ||
| 230 | <div> | 241 | <div> | ||
| 231 | <dt> | 242 | <dt> | ||
M internal/app/testdata/web/commits-path.html +14 -3
| @@ -220,14 +220,25 @@ | |||
| 220 | 220 | <dt> | |
| 221 | 221 | <kbd>enter</kbd> | |
| 222 | 222 | </dt> | |
| 223 | - | <dd>open the row under the cursor</dd> | |
| 223 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 224 | + | </div> | |
| 225 | + | <div> | |
| 226 | + | <dt> | |
| 227 | + | <kbd>↑</kbd> | |
| 228 | + | <kbd>↓</kbd> | |
| 229 | + | <kbd>←</kbd> | |
| 230 | + | <kbd>→</kbd> | |
| 231 | + | <kbd>home</kbd> | |
| 232 | + | <kbd>end</kbd> | |
| 233 | + | </dt> | |
| 234 | + | <dd>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</dd> | |
| 224 | 235 | </div> | |
| 225 | 236 | <div> | |
| 226 | 237 | <dt> | |
| 227 | 238 | <kbd>l</kbd> | |
| 228 | 239 | <kbd>h</kbd> | |
| 229 | 240 | </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> | |
| 241 | + | <dd>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</dd> | |
| 231 | 242 | </div> | |
| 232 | 243 | <div> | |
| 233 | 244 | <dt> | |
| @@ -240,7 +251,7 @@ | |||
| 240 | 251 | <dt> | |
| 241 | 252 | <kbd>/</kbd> | |
| 242 | 253 | </dt> | |
| 243 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 254 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 244 | 255 | </div> | |
| 245 | 256 | <div> | |
| 246 | 257 | <dt> | |
| @@ -220,14 +220,25 @@ | |||||
| 220 | <dt> | 220 | <dt> | ||
| 221 | <kbd>enter</kbd> | 221 | <kbd>enter</kbd> | ||
| 222 | </dt> | 222 | </dt> | ||
| 223 | - | <dd>open the row under the cursor</dd> | 223 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 224 | + | </div> | |||
| 225 | + | <div> | |||
| 226 | + | <dt> | |||
| 227 | + | <kbd>↑</kbd> | |||
| 228 | + | <kbd>↓</kbd> | |||
| 229 | + | <kbd>←</kbd> | |||
| 230 | + | <kbd>→</kbd> | |||
| 231 | + | <kbd>home</kbd> | |||
| 232 | + | <kbd>end</kbd> | |||
| 233 | + | </dt> | |||
| 234 | + | <dd>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</dd> | |||
| 224 | </div> | 235 | </div> | ||
| 225 | <div> | 236 | <div> | ||
| 226 | <dt> | 237 | <dt> | ||
| 227 | <kbd>l</kbd> | 238 | <kbd>l</kbd> | ||
| 228 | <kbd>h</kbd> | 239 | <kbd>h</kbd> | ||
| 229 | </dt> | 240 | </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> | 241 | + | <dd>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</dd> |
| 231 | </div> | 242 | </div> | ||
| 232 | <div> | 243 | <div> | ||
| 233 | <dt> | 244 | <dt> | ||
| @@ -240,7 +251,7 @@ | |||||
| 240 | <dt> | 251 | <dt> | ||
| 241 | <kbd>/</kbd> | 252 | <kbd>/</kbd> | ||
| 242 | </dt> | 253 | </dt> | ||
| 243 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 254 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 244 | </div> | 255 | </div> | ||
| 245 | <div> | 256 | <div> | ||
| 246 | <dt> | 257 | <dt> | ||
M internal/app/testdata/web/commits-root.html +14 -3
| @@ -1105,14 +1105,25 @@ | |||
| 1105 | 1105 | <dt> | |
| 1106 | 1106 | <kbd>enter</kbd> | |
| 1107 | 1107 | </dt> | |
| 1108 | - | <dd>open the row under the cursor</dd> | |
| 1108 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 1109 | + | </div> | |
| 1110 | + | <div> | |
| 1111 | + | <dt> | |
| 1112 | + | <kbd>↑</kbd> | |
| 1113 | + | <kbd>↓</kbd> | |
| 1114 | + | <kbd>←</kbd> | |
| 1115 | + | <kbd>→</kbd> | |
| 1116 | + | <kbd>home</kbd> | |
| 1117 | + | <kbd>end</kbd> | |
| 1118 | + | </dt> | |
| 1119 | + | <dd>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</dd> | |
| 1109 | 1120 | </div> | |
| 1110 | 1121 | <div> | |
| 1111 | 1122 | <dt> | |
| 1112 | 1123 | <kbd>l</kbd> | |
| 1113 | 1124 | <kbd>h</kbd> | |
| 1114 | 1125 | </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> | |
| 1126 | + | <dd>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</dd> | |
| 1116 | 1127 | </div> | |
| 1117 | 1128 | <div> | |
| 1118 | 1129 | <dt> | |
| @@ -1125,7 +1136,7 @@ | |||
| 1125 | 1136 | <dt> | |
| 1126 | 1137 | <kbd>/</kbd> | |
| 1127 | 1138 | </dt> | |
| 1128 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 1139 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 1129 | 1140 | </div> | |
| 1130 | 1141 | <div> | |
| 1131 | 1142 | <dt> | |
| @@ -1105,14 +1105,25 @@ | |||||
| 1105 | <dt> | 1105 | <dt> | ||
| 1106 | <kbd>enter</kbd> | 1106 | <kbd>enter</kbd> | ||
| 1107 | </dt> | 1107 | </dt> | ||
| 1108 | - | <dd>open the row under the cursor</dd> | 1108 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 1109 | + | </div> | |||
| 1110 | + | <div> | |||
| 1111 | + | <dt> | |||
| 1112 | + | <kbd>↑</kbd> | |||
| 1113 | + | <kbd>↓</kbd> | |||
| 1114 | + | <kbd>←</kbd> | |||
| 1115 | + | <kbd>→</kbd> | |||
| 1116 | + | <kbd>home</kbd> | |||
| 1117 | + | <kbd>end</kbd> | |||
| 1118 | + | </dt> | |||
| 1119 | + | <dd>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</dd> | |||
| 1109 | </div> | 1120 | </div> | ||
| 1110 | <div> | 1121 | <div> | ||
| 1111 | <dt> | 1122 | <dt> | ||
| 1112 | <kbd>l</kbd> | 1123 | <kbd>l</kbd> | ||
| 1113 | <kbd>h</kbd> | 1124 | <kbd>h</kbd> | ||
| 1114 | </dt> | 1125 | </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> | 1126 | + | <dd>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</dd> |
| 1116 | </div> | 1127 | </div> | ||
| 1117 | <div> | 1128 | <div> | ||
| 1118 | <dt> | 1129 | <dt> | ||
| @@ -1125,7 +1136,7 @@ | |||||
| 1125 | <dt> | 1136 | <dt> | ||
| 1126 | <kbd>/</kbd> | 1137 | <kbd>/</kbd> | ||
| 1127 | </dt> | 1138 | </dt> | ||
| 1128 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 1139 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 1129 | </div> | 1140 | </div> | ||
| 1130 | <div> | 1141 | <div> | ||
| 1131 | <dt> | 1142 | <dt> | ||
M internal/app/testdata/web/commits-small.html +14 -3
| @@ -366,14 +366,25 @@ | |||
| 366 | 366 | <dt> | |
| 367 | 367 | <kbd>enter</kbd> | |
| 368 | 368 | </dt> | |
| 369 | - | <dd>open the row under the cursor</dd> | |
| 369 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 370 | + | </div> | |
| 371 | + | <div> | |
| 372 | + | <dt> | |
| 373 | + | <kbd>↑</kbd> | |
| 374 | + | <kbd>↓</kbd> | |
| 375 | + | <kbd>←</kbd> | |
| 376 | + | <kbd>→</kbd> | |
| 377 | + | <kbd>home</kbd> | |
| 378 | + | <kbd>end</kbd> | |
| 379 | + | </dt> | |
| 380 | + | <dd>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</dd> | |
| 370 | 381 | </div> | |
| 371 | 382 | <div> | |
| 372 | 383 | <dt> | |
| 373 | 384 | <kbd>l</kbd> | |
| 374 | 385 | <kbd>h</kbd> | |
| 375 | 386 | </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> | |
| 387 | + | <dd>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</dd> | |
| 377 | 388 | </div> | |
| 378 | 389 | <div> | |
| 379 | 390 | <dt> | |
| @@ -386,7 +397,7 @@ | |||
| 386 | 397 | <dt> | |
| 387 | 398 | <kbd>/</kbd> | |
| 388 | 399 | </dt> | |
| 389 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 400 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 390 | 401 | </div> | |
| 391 | 402 | <div> | |
| 392 | 403 | <dt> | |
| @@ -366,14 +366,25 @@ | |||||
| 366 | <dt> | 366 | <dt> | ||
| 367 | <kbd>enter</kbd> | 367 | <kbd>enter</kbd> | ||
| 368 | </dt> | 368 | </dt> | ||
| 369 | - | <dd>open the row under the cursor</dd> | 369 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 370 | + | </div> | |||
| 371 | + | <div> | |||
| 372 | + | <dt> | |||
| 373 | + | <kbd>↑</kbd> | |||
| 374 | + | <kbd>↓</kbd> | |||
| 375 | + | <kbd>←</kbd> | |||
| 376 | + | <kbd>→</kbd> | |||
| 377 | + | <kbd>home</kbd> | |||
| 378 | + | <kbd>end</kbd> | |||
| 379 | + | </dt> | |||
| 380 | + | <dd>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</dd> | |||
| 370 | </div> | 381 | </div> | ||
| 371 | <div> | 382 | <div> | ||
| 372 | <dt> | 383 | <dt> | ||
| 373 | <kbd>l</kbd> | 384 | <kbd>l</kbd> | ||
| 374 | <kbd>h</kbd> | 385 | <kbd>h</kbd> | ||
| 375 | </dt> | 386 | </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> | 387 | + | <dd>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</dd> |
| 377 | </div> | 388 | </div> | ||
| 378 | <div> | 389 | <div> | ||
| 379 | <dt> | 390 | <dt> | ||
| @@ -386,7 +397,7 @@ | |||||
| 386 | <dt> | 397 | <dt> | ||
| 387 | <kbd>/</kbd> | 398 | <kbd>/</kbd> | ||
| 388 | </dt> | 399 | </dt> | ||
| 389 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 400 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 390 | </div> | 401 | </div> | ||
| 391 | <div> | 402 | <div> | ||
| 392 | <dt> | 403 | <dt> | ||
M internal/app/testdata/web/empty.html +14 -3
| @@ -123,14 +123,25 @@ | |||
| 123 | 123 | <dt> | |
| 124 | 124 | <kbd>enter</kbd> | |
| 125 | 125 | </dt> | |
| 126 | - | <dd>open the row under the cursor</dd> | |
| 126 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 127 | + | </div> | |
| 128 | + | <div> | |
| 129 | + | <dt> | |
| 130 | + | <kbd>↑</kbd> | |
| 131 | + | <kbd>↓</kbd> | |
| 132 | + | <kbd>←</kbd> | |
| 133 | + | <kbd>→</kbd> | |
| 134 | + | <kbd>home</kbd> | |
| 135 | + | <kbd>end</kbd> | |
| 136 | + | </dt> | |
| 137 | + | <dd>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</dd> | |
| 127 | 138 | </div> | |
| 128 | 139 | <div> | |
| 129 | 140 | <dt> | |
| 130 | 141 | <kbd>l</kbd> | |
| 131 | 142 | <kbd>h</kbd> | |
| 132 | 143 | </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> | |
| 144 | + | <dd>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</dd> | |
| 134 | 145 | </div> | |
| 135 | 146 | <div> | |
| 136 | 147 | <dt> | |
| @@ -143,7 +154,7 @@ | |||
| 143 | 154 | <dt> | |
| 144 | 155 | <kbd>/</kbd> | |
| 145 | 156 | </dt> | |
| 146 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 157 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 147 | 158 | </div> | |
| 148 | 159 | <div> | |
| 149 | 160 | <dt> | |
| @@ -123,14 +123,25 @@ | |||||
| 123 | <dt> | 123 | <dt> | ||
| 124 | <kbd>enter</kbd> | 124 | <kbd>enter</kbd> | ||
| 125 | </dt> | 125 | </dt> | ||
| 126 | - | <dd>open the row under the cursor</dd> | 126 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 127 | + | </div> | |||
| 128 | + | <div> | |||
| 129 | + | <dt> | |||
| 130 | + | <kbd>↑</kbd> | |||
| 131 | + | <kbd>↓</kbd> | |||
| 132 | + | <kbd>←</kbd> | |||
| 133 | + | <kbd>→</kbd> | |||
| 134 | + | <kbd>home</kbd> | |||
| 135 | + | <kbd>end</kbd> | |||
| 136 | + | </dt> | |||
| 137 | + | <dd>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</dd> | |||
| 127 | </div> | 138 | </div> | ||
| 128 | <div> | 139 | <div> | ||
| 129 | <dt> | 140 | <dt> | ||
| 130 | <kbd>l</kbd> | 141 | <kbd>l</kbd> | ||
| 131 | <kbd>h</kbd> | 142 | <kbd>h</kbd> | ||
| 132 | </dt> | 143 | </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> | 144 | + | <dd>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</dd> |
| 134 | </div> | 145 | </div> | ||
| 135 | <div> | 146 | <div> | ||
| 136 | <dt> | 147 | <dt> | ||
| @@ -143,7 +154,7 @@ | |||||
| 143 | <dt> | 154 | <dt> | ||
| 144 | <kbd>/</kbd> | 155 | <kbd>/</kbd> | ||
| 145 | </dt> | 156 | </dt> | ||
| 146 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 157 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 147 | </div> | 158 | </div> | ||
| 148 | <div> | 159 | <div> | ||
| 149 | <dt> | 160 | <dt> | ||
M internal/app/testdata/web/home.html +14 -3
| @@ -148,14 +148,25 @@ | |||
| 148 | 148 | <dt> | |
| 149 | 149 | <kbd>enter</kbd> | |
| 150 | 150 | </dt> | |
| 151 | - | <dd>open the row under the cursor</dd> | |
| 151 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 152 | + | </div> | |
| 153 | + | <div> | |
| 154 | + | <dt> | |
| 155 | + | <kbd>↑</kbd> | |
| 156 | + | <kbd>↓</kbd> | |
| 157 | + | <kbd>←</kbd> | |
| 158 | + | <kbd>→</kbd> | |
| 159 | + | <kbd>home</kbd> | |
| 160 | + | <kbd>end</kbd> | |
| 161 | + | </dt> | |
| 162 | + | <dd>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</dd> | |
| 152 | 163 | </div> | |
| 153 | 164 | <div> | |
| 154 | 165 | <dt> | |
| 155 | 166 | <kbd>l</kbd> | |
| 156 | 167 | <kbd>h</kbd> | |
| 157 | 168 | </dt> | |
| 158 | - | <dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd> | |
| 169 | + | <dd>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</dd> | |
| 159 | 170 | </div> | |
| 160 | 171 | <div> | |
| 161 | 172 | <dt> | |
| @@ -168,7 +179,7 @@ | |||
| 168 | 179 | <dt> | |
| 169 | 180 | <kbd>/</kbd> | |
| 170 | 181 | </dt> | |
| 171 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 182 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 172 | 183 | </div> | |
| 173 | 184 | <div> | |
| 174 | 185 | <dt> | |
| @@ -148,14 +148,25 @@ | |||||
| 148 | <dt> | 148 | <dt> | ||
| 149 | <kbd>enter</kbd> | 149 | <kbd>enter</kbd> | ||
| 150 | </dt> | 150 | </dt> | ||
| 151 | - | <dd>open the row under the cursor</dd> | 151 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 152 | + | </div> | |||
| 153 | + | <div> | |||
| 154 | + | <dt> | |||
| 155 | + | <kbd>↑</kbd> | |||
| 156 | + | <kbd>↓</kbd> | |||
| 157 | + | <kbd>←</kbd> | |||
| 158 | + | <kbd>→</kbd> | |||
| 159 | + | <kbd>home</kbd> | |||
| 160 | + | <kbd>end</kbd> | |||
| 161 | + | </dt> | |||
| 162 | + | <dd>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</dd> | |||
| 152 | </div> | 163 | </div> | ||
| 153 | <div> | 164 | <div> | ||
| 154 | <dt> | 165 | <dt> | ||
| 155 | <kbd>l</kbd> | 166 | <kbd>l</kbd> | ||
| 156 | <kbd>h</kbd> | 167 | <kbd>h</kbd> | ||
| 157 | </dt> | 168 | </dt> | ||
| 158 | - | <dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd> | 169 | + | <dd>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</dd> |
| 159 | </div> | 170 | </div> | ||
| 160 | <div> | 171 | <div> | ||
| 161 | <dt> | 172 | <dt> | ||
| @@ -168,7 +179,7 @@ | |||||
| 168 | <dt> | 179 | <dt> | ||
| 169 | <kbd>/</kbd> | 180 | <kbd>/</kbd> | ||
| 170 | </dt> | 181 | </dt> | ||
| 171 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 182 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 172 | </div> | 183 | </div> | ||
| 173 | <div> | 184 | <div> | ||
| 174 | <dt> | 185 | <dt> | ||
M internal/app/testdata/web/not-found-page.html +14 -3
| @@ -112,14 +112,25 @@ | |||
| 112 | 112 | <dt> | |
| 113 | 113 | <kbd>enter</kbd> | |
| 114 | 114 | </dt> | |
| 115 | - | <dd>open the row under the cursor</dd> | |
| 115 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 116 | + | </div> | |
| 117 | + | <div> | |
| 118 | + | <dt> | |
| 119 | + | <kbd>↑</kbd> | |
| 120 | + | <kbd>↓</kbd> | |
| 121 | + | <kbd>←</kbd> | |
| 122 | + | <kbd>→</kbd> | |
| 123 | + | <kbd>home</kbd> | |
| 124 | + | <kbd>end</kbd> | |
| 125 | + | </dt> | |
| 126 | + | <dd>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</dd> | |
| 116 | 127 | </div> | |
| 117 | 128 | <div> | |
| 118 | 129 | <dt> | |
| 119 | 130 | <kbd>l</kbd> | |
| 120 | 131 | <kbd>h</kbd> | |
| 121 | 132 | </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> | |
| 133 | + | <dd>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</dd> | |
| 123 | 134 | </div> | |
| 124 | 135 | <div> | |
| 125 | 136 | <dt> | |
| @@ -132,7 +143,7 @@ | |||
| 132 | 143 | <dt> | |
| 133 | 144 | <kbd>/</kbd> | |
| 134 | 145 | </dt> | |
| 135 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 146 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 136 | 147 | </div> | |
| 137 | 148 | <div> | |
| 138 | 149 | <dt> | |
| @@ -112,14 +112,25 @@ | |||||
| 112 | <dt> | 112 | <dt> | ||
| 113 | <kbd>enter</kbd> | 113 | <kbd>enter</kbd> | ||
| 114 | </dt> | 114 | </dt> | ||
| 115 | - | <dd>open the row under the cursor</dd> | 115 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 116 | + | </div> | |||
| 117 | + | <div> | |||
| 118 | + | <dt> | |||
| 119 | + | <kbd>↑</kbd> | |||
| 120 | + | <kbd>↓</kbd> | |||
| 121 | + | <kbd>←</kbd> | |||
| 122 | + | <kbd>→</kbd> | |||
| 123 | + | <kbd>home</kbd> | |||
| 124 | + | <kbd>end</kbd> | |||
| 125 | + | </dt> | |||
| 126 | + | <dd>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</dd> | |||
| 116 | </div> | 127 | </div> | ||
| 117 | <div> | 128 | <div> | ||
| 118 | <dt> | 129 | <dt> | ||
| 119 | <kbd>l</kbd> | 130 | <kbd>l</kbd> | ||
| 120 | <kbd>h</kbd> | 131 | <kbd>h</kbd> | ||
| 121 | </dt> | 132 | </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> | 133 | + | <dd>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</dd> |
| 123 | </div> | 134 | </div> | ||
| 124 | <div> | 135 | <div> | ||
| 125 | <dt> | 136 | <dt> | ||
| @@ -132,7 +143,7 @@ | |||||
| 132 | <dt> | 143 | <dt> | ||
| 133 | <kbd>/</kbd> | 144 | <kbd>/</kbd> | ||
| 134 | </dt> | 145 | </dt> | ||
| 135 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 146 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 136 | </div> | 147 | </div> | ||
| 137 | <div> | 148 | <div> | ||
| 138 | <dt> | 149 | <dt> | ||
M internal/app/testdata/web/not-found.html +14 -3
| @@ -112,14 +112,25 @@ | |||
| 112 | 112 | <dt> | |
| 113 | 113 | <kbd>enter</kbd> | |
| 114 | 114 | </dt> | |
| 115 | - | <dd>open the row under the cursor</dd> | |
| 115 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 116 | + | </div> | |
| 117 | + | <div> | |
| 118 | + | <dt> | |
| 119 | + | <kbd>↑</kbd> | |
| 120 | + | <kbd>↓</kbd> | |
| 121 | + | <kbd>←</kbd> | |
| 122 | + | <kbd>→</kbd> | |
| 123 | + | <kbd>home</kbd> | |
| 124 | + | <kbd>end</kbd> | |
| 125 | + | </dt> | |
| 126 | + | <dd>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</dd> | |
| 116 | 127 | </div> | |
| 117 | 128 | <div> | |
| 118 | 129 | <dt> | |
| 119 | 130 | <kbd>l</kbd> | |
| 120 | 131 | <kbd>h</kbd> | |
| 121 | 132 | </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> | |
| 133 | + | <dd>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</dd> | |
| 123 | 134 | </div> | |
| 124 | 135 | <div> | |
| 125 | 136 | <dt> | |
| @@ -132,7 +143,7 @@ | |||
| 132 | 143 | <dt> | |
| 133 | 144 | <kbd>/</kbd> | |
| 134 | 145 | </dt> | |
| 135 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 146 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 136 | 147 | </div> | |
| 137 | 148 | <div> | |
| 138 | 149 | <dt> | |
| @@ -112,14 +112,25 @@ | |||||
| 112 | <dt> | 112 | <dt> | ||
| 113 | <kbd>enter</kbd> | 113 | <kbd>enter</kbd> | ||
| 114 | </dt> | 114 | </dt> | ||
| 115 | - | <dd>open the row under the cursor</dd> | 115 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 116 | + | </div> | |||
| 117 | + | <div> | |||
| 118 | + | <dt> | |||
| 119 | + | <kbd>↑</kbd> | |||
| 120 | + | <kbd>↓</kbd> | |||
| 121 | + | <kbd>←</kbd> | |||
| 122 | + | <kbd>→</kbd> | |||
| 123 | + | <kbd>home</kbd> | |||
| 124 | + | <kbd>end</kbd> | |||
| 125 | + | </dt> | |||
| 126 | + | <dd>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</dd> | |||
| 116 | </div> | 127 | </div> | ||
| 117 | <div> | 128 | <div> | ||
| 118 | <dt> | 129 | <dt> | ||
| 119 | <kbd>l</kbd> | 130 | <kbd>l</kbd> | ||
| 120 | <kbd>h</kbd> | 131 | <kbd>h</kbd> | ||
| 121 | </dt> | 132 | </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> | 133 | + | <dd>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</dd> |
| 123 | </div> | 134 | </div> | ||
| 124 | <div> | 135 | <div> | ||
| 125 | <dt> | 136 | <dt> | ||
| @@ -132,7 +143,7 @@ | |||||
| 132 | <dt> | 143 | <dt> | ||
| 133 | <kbd>/</kbd> | 144 | <kbd>/</kbd> | ||
| 134 | </dt> | 145 | </dt> | ||
| 135 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 146 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 136 | </div> | 147 | </div> | ||
| 137 | <div> | 148 | <div> | ||
| 138 | <dt> | 149 | <dt> | ||
M internal/app/testdata/web/refs.html +14 -3
| @@ -242,14 +242,25 @@ | |||
| 242 | 242 | <dt> | |
| 243 | 243 | <kbd>enter</kbd> | |
| 244 | 244 | </dt> | |
| 245 | - | <dd>open the row under the cursor</dd> | |
| 245 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 246 | + | </div> | |
| 247 | + | <div> | |
| 248 | + | <dt> | |
| 249 | + | <kbd>↑</kbd> | |
| 250 | + | <kbd>↓</kbd> | |
| 251 | + | <kbd>←</kbd> | |
| 252 | + | <kbd>→</kbd> | |
| 253 | + | <kbd>home</kbd> | |
| 254 | + | <kbd>end</kbd> | |
| 255 | + | </dt> | |
| 256 | + | <dd>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</dd> | |
| 246 | 257 | </div> | |
| 247 | 258 | <div> | |
| 248 | 259 | <dt> | |
| 249 | 260 | <kbd>l</kbd> | |
| 250 | 261 | <kbd>h</kbd> | |
| 251 | 262 | </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> | |
| 263 | + | <dd>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</dd> | |
| 253 | 264 | </div> | |
| 254 | 265 | <div> | |
| 255 | 266 | <dt> | |
| @@ -262,7 +273,7 @@ | |||
| 262 | 273 | <dt> | |
| 263 | 274 | <kbd>/</kbd> | |
| 264 | 275 | </dt> | |
| 265 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 276 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 266 | 277 | </div> | |
| 267 | 278 | <div> | |
| 268 | 279 | <dt> | |
| @@ -242,14 +242,25 @@ | |||||
| 242 | <dt> | 242 | <dt> | ||
| 243 | <kbd>enter</kbd> | 243 | <kbd>enter</kbd> | ||
| 244 | </dt> | 244 | </dt> | ||
| 245 | - | <dd>open the row under the cursor</dd> | 245 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 246 | + | </div> | |||
| 247 | + | <div> | |||
| 248 | + | <dt> | |||
| 249 | + | <kbd>↑</kbd> | |||
| 250 | + | <kbd>↓</kbd> | |||
| 251 | + | <kbd>←</kbd> | |||
| 252 | + | <kbd>→</kbd> | |||
| 253 | + | <kbd>home</kbd> | |||
| 254 | + | <kbd>end</kbd> | |||
| 255 | + | </dt> | |||
| 256 | + | <dd>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</dd> | |||
| 246 | </div> | 257 | </div> | ||
| 247 | <div> | 258 | <div> | ||
| 248 | <dt> | 259 | <dt> | ||
| 249 | <kbd>l</kbd> | 260 | <kbd>l</kbd> | ||
| 250 | <kbd>h</kbd> | 261 | <kbd>h</kbd> | ||
| 251 | </dt> | 262 | </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> | 263 | + | <dd>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</dd> |
| 253 | </div> | 264 | </div> | ||
| 254 | <div> | 265 | <div> | ||
| 255 | <dt> | 266 | <dt> | ||
| @@ -262,7 +273,7 @@ | |||||
| 262 | <dt> | 273 | <dt> | ||
| 263 | <kbd>/</kbd> | 274 | <kbd>/</kbd> | ||
| 264 | </dt> | 275 | </dt> | ||
| 265 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 276 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 266 | </div> | 277 | </div> | ||
| 267 | <div> | 278 | <div> | ||
| 268 | <dt> | 279 | <dt> | ||
M internal/app/testdata/web/summary.html +14 -3
| @@ -266,14 +266,25 @@ | |||
| 266 | 266 | <dt> | |
| 267 | 267 | <kbd>enter</kbd> | |
| 268 | 268 | </dt> | |
| 269 | - | <dd>open the row under the cursor</dd> | |
| 269 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 270 | + | </div> | |
| 271 | + | <div> | |
| 272 | + | <dt> | |
| 273 | + | <kbd>↑</kbd> | |
| 274 | + | <kbd>↓</kbd> | |
| 275 | + | <kbd>←</kbd> | |
| 276 | + | <kbd>→</kbd> | |
| 277 | + | <kbd>home</kbd> | |
| 278 | + | <kbd>end</kbd> | |
| 279 | + | </dt> | |
| 280 | + | <dd>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</dd> | |
| 270 | 281 | </div> | |
| 271 | 282 | <div> | |
| 272 | 283 | <dt> | |
| 273 | 284 | <kbd>l</kbd> | |
| 274 | 285 | <kbd>h</kbd> | |
| 275 | 286 | </dt> | |
| 276 | - | <dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd> | |
| 287 | + | <dd>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</dd> | |
| 277 | 288 | </div> | |
| 278 | 289 | <div> | |
| 279 | 290 | <dt> | |
| @@ -286,7 +297,7 @@ | |||
| 286 | 297 | <dt> | |
| 287 | 298 | <kbd>/</kbd> | |
| 288 | 299 | </dt> | |
| 289 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 300 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 290 | 301 | </div> | |
| 291 | 302 | <div> | |
| 292 | 303 | <dt> | |
| @@ -266,14 +266,25 @@ | |||||
| 266 | <dt> | 266 | <dt> | ||
| 267 | <kbd>enter</kbd> | 267 | <kbd>enter</kbd> | ||
| 268 | </dt> | 268 | </dt> | ||
| 269 | - | <dd>open the row under the cursor</dd> | 269 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 270 | + | </div> | |||
| 271 | + | <div> | |||
| 272 | + | <dt> | |||
| 273 | + | <kbd>↑</kbd> | |||
| 274 | + | <kbd>↓</kbd> | |||
| 275 | + | <kbd>←</kbd> | |||
| 276 | + | <kbd>→</kbd> | |||
| 277 | + | <kbd>home</kbd> | |||
| 278 | + | <kbd>end</kbd> | |||
| 279 | + | </dt> | |||
| 280 | + | <dd>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</dd> | |||
| 270 | </div> | 281 | </div> | ||
| 271 | <div> | 282 | <div> | ||
| 272 | <dt> | 283 | <dt> | ||
| 273 | <kbd>l</kbd> | 284 | <kbd>l</kbd> | ||
| 274 | <kbd>h</kbd> | 285 | <kbd>h</kbd> | ||
| 275 | </dt> | 286 | </dt> | ||
| 276 | - | <dd>into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd> | 287 | + | <dd>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</dd> |
| 277 | </div> | 288 | </div> | ||
| 278 | <div> | 289 | <div> | ||
| 279 | <dt> | 290 | <dt> | ||
| @@ -286,7 +297,7 @@ | |||||
| 286 | <dt> | 297 | <dt> | ||
| 287 | <kbd>/</kbd> | 298 | <kbd>/</kbd> | ||
| 288 | </dt> | 299 | </dt> | ||
| 289 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 300 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 290 | </div> | 301 | </div> | ||
| 291 | <div> | 302 | <div> | ||
| 292 | <dt> | 303 | <dt> | ||
M internal/app/testdata/web/tree-nested.html +45 -39
| @@ -102,50 +102,45 @@ | |||
| 102 | 102 | </header> | |
| 103 | 103 | <div class="work"> | |
| 104 | 104 | <main id="main" tabindex="-1"> | |
| 105 | - | <div class="filter"> | |
| 105 | + | <div class="tree-find"> | |
| 106 | + | <div class="filter wide"> | |
| 106 | 107 | <label for="finder-q" class="sr">find a file</label> | |
| 107 | - | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | |
| 108 | + | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file in this repository…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | |
| 108 | 109 | </div> | |
| 109 | - | <div id="finder" hidden> | |
| 110 | + | <p class="tree-keys" data-js aria-hidden="true"> | |
| 111 | + | <kbd>j</kbd> | |
| 112 | + | <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p> | |
| 113 | + | </div> | |
| 114 | + | <div id="finder" class="tree-card" hidden> | |
| 110 | 115 | <p id="finder-status" class="dim" aria-live="polite"> | |
| 111 | 116 | </p> | |
| 112 | 117 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | |
| 113 | 118 | </ul> | |
| 114 | 119 | </div> | |
| 115 | - | <div id="tree-view"> | |
| 116 | - | <table class="list tree" aria-label="Entries"> | |
| 117 | - | <thead> | |
| 118 | - | <tr> | |
| 119 | - | <th class="cur"> | |
| 120 | - | </th> | |
| 121 | - | <th class="c-kind"> | |
| 122 | - | </th> | |
| 123 | - | <th class="c-name">name</th> | |
| 124 | - | <th class="c-size">size</th> | |
| 125 | - | <th class="c-sub">last commit</th> | |
| 126 | - | <th class="c-who">author</th> | |
| 127 | - | <th class="c-when">date</th> | |
| 128 | - | </tr> | |
| 129 | - | </thead> | |
| 130 | - | <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)"> | |
| 131 | - | <tr data-cursor> | |
| 132 | - | <td class="cur" aria-hidden="true"> | |
| 133 | - | </td> | |
| 134 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 135 | - | <td class="c-name"> | |
| 136 | - | <a href="/files/blob/main/src/lib/util.go">util.go</a> | |
| 137 | - | </td> | |
| 138 | - | <td class="c-size num dim">12 B</td> | |
| 139 | - | <td class="c-sub dim"> | |
| 140 | - | <a href="/files/commit/SHA40">add src/lib/util.go</a> | |
| 141 | - | </td> | |
| 142 | - | <td class="c-who dim">T</td> | |
| 143 | - | <td class="c-when nowrap dim"> | |
| 120 | + | <div id="tree-view" class="tree-card"> | |
| 121 | + | <div class="tree-head"> | |
| 122 | + | <span class="tree-path">src/lib/</span> | |
| 123 | + | <a class="tree-up" href="/files/tree/main/src" title="parent directory (h)">..</a> | |
| 124 | + | <span class="tree-count">1 entry</span> | |
| 125 | + | </div> | |
| 126 | + | <ul id="tree-rows" class="tree" role="tree" aria-label="Entries" data-rows data-tree data-up="/files/tree/main/src" hx-on:click="ermiteTree.click(event)"> | |
| 127 | + | <li role="treeitem" aria-level="1" aria-posinset="1" aria-setsize="1" data-kind="file" data-depth="0" data-cursor> | |
| 128 | + | <span class="t-main"> | |
| 129 | + | <span class="t-twig" aria-hidden="true"> | |
| 130 | + | </span> | |
| 131 | + | <span class="t-node" aria-hidden="true"> | |
| 132 | + | </span> | |
| 133 | + | <a class="t-name" href="/files/blob/main/src/lib/util.go">util.go</a> | |
| 134 | + | </span> | |
| 135 | + | <span class="t-size">12 B</span> | |
| 136 | + | <span class="t-sub"> | |
| 137 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add src/lib/util.go</a> | |
| 138 | + | </span> | |
| 139 | + | <span class="t-when"> | |
| 144 | 140 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 145 | - | </td> | |
| 146 | - | </tr> | |
| 147 | - | </tbody> | |
| 148 | - | </table> | |
| 141 | + | </span> | |
| 142 | + | </li> | |
| 143 | + | </ul> | |
| 149 | 144 | </div> | |
| 150 | 145 | </main> | |
| 151 | 146 | </div> | |
| @@ -194,14 +189,25 @@ | |||
| 194 | 189 | <dt> | |
| 195 | 190 | <kbd>enter</kbd> | |
| 196 | 191 | </dt> | |
| 197 | - | <dd>open the row under the cursor</dd> | |
| 192 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 193 | + | </div> | |
| 194 | + | <div> | |
| 195 | + | <dt> | |
| 196 | + | <kbd>↑</kbd> | |
| 197 | + | <kbd>↓</kbd> | |
| 198 | + | <kbd>←</kbd> | |
| 199 | + | <kbd>→</kbd> | |
| 200 | + | <kbd>home</kbd> | |
| 201 | + | <kbd>end</kbd> | |
| 202 | + | </dt> | |
| 203 | + | <dd>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</dd> | |
| 198 | 204 | </div> | |
| 199 | 205 | <div> | |
| 200 | 206 | <dt> | |
| 201 | 207 | <kbd>l</kbd> | |
| 202 | 208 | <kbd>h</kbd> | |
| 203 | 209 | </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> | |
| 210 | + | <dd>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</dd> | |
| 205 | 211 | </div> | |
| 206 | 212 | <div> | |
| 207 | 213 | <dt> | |
| @@ -214,7 +220,7 @@ | |||
| 214 | 220 | <dt> | |
| 215 | 221 | <kbd>/</kbd> | |
| 216 | 222 | </dt> | |
| 217 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 223 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 218 | 224 | </div> | |
| 219 | 225 | <div> | |
| 220 | 226 | <dt> | |
| @@ -102,50 +102,45 @@ | |||||
| 102 | </header> | 102 | </header> | ||
| 103 | <div class="work"> | 103 | <div class="work"> | ||
| 104 | <main id="main" tabindex="-1"> | 104 | <main id="main" tabindex="-1"> | ||
| 105 | - | <div class="filter"> | 105 | + | <div class="tree-find"> |
| 106 | + | <div class="filter wide"> | |||
| 106 | <label for="finder-q" class="sr">find a file</label> | 107 | <label for="finder-q" class="sr">find a file</label> | ||
| 107 | - | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | 108 | + | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file in this repository…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> |
| 108 | </div> | 109 | </div> | ||
| 109 | - | <div id="finder" hidden> | 110 | + | <p class="tree-keys" data-js aria-hidden="true"> |
| 111 | + | <kbd>j</kbd> | |||
| 112 | + | <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p> | |||
| 113 | + | </div> | |||
| 114 | + | <div id="finder" class="tree-card" hidden> | |||
| 110 | <p id="finder-status" class="dim" aria-live="polite"> | 115 | <p id="finder-status" class="dim" aria-live="polite"> | ||
| 111 | </p> | 116 | </p> | ||
| 112 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | 117 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | ||
| 113 | </ul> | 118 | </ul> | ||
| 114 | </div> | 119 | </div> | ||
| 115 | - | <div id="tree-view"> | 120 | + | <div id="tree-view" class="tree-card"> |
| 116 | - | <table class="list tree" aria-label="Entries"> | 121 | + | <div class="tree-head"> |
| 117 | - | <thead> | 122 | + | <span class="tree-path">src/lib/</span> |
| 118 | - | <tr> | 123 | + | <a class="tree-up" href="/files/tree/main/src" title="parent directory (h)">..</a> |
| 119 | - | <th class="cur"> | 124 | + | <span class="tree-count">1 entry</span> |
| 120 | - | </th> | 125 | + | </div> |
| 121 | - | <th class="c-kind"> | 126 | + | <ul id="tree-rows" class="tree" role="tree" aria-label="Entries" data-rows data-tree data-up="/files/tree/main/src" hx-on:click="ermiteTree.click(event)"> |
| 122 | - | </th> | 127 | + | <li role="treeitem" aria-level="1" aria-posinset="1" aria-setsize="1" data-kind="file" data-depth="0" data-cursor> |
| 123 | - | <th class="c-name">name</th> | 128 | + | <span class="t-main"> |
| 124 | - | <th class="c-size">size</th> | 129 | + | <span class="t-twig" aria-hidden="true"> |
| 125 | - | <th class="c-sub">last commit</th> | 130 | + | </span> |
| 126 | - | <th class="c-who">author</th> | 131 | + | <span class="t-node" aria-hidden="true"> |
| 127 | - | <th class="c-when">date</th> | 132 | + | </span> |
| 128 | - | </tr> | 133 | + | <a class="t-name" href="/files/blob/main/src/lib/util.go">util.go</a> |
| 129 | - | </thead> | 134 | + | </span> |
| 130 | - | <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)"> | 135 | + | <span class="t-size">12 B</span> |
| 131 | - | <tr data-cursor> | 136 | + | <span class="t-sub"> |
| 132 | - | <td class="cur" aria-hidden="true"> | 137 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add src/lib/util.go</a> |
| 133 | - | </td> | 138 | + | </span> |
| 134 | - | <td class="c-kind" aria-hidden="true">·</td> | 139 | + | <span class="t-when"> |
| 135 | - | <td class="c-name"> | |||
| 136 | - | <a href="/files/blob/main/src/lib/util.go">util.go</a> | |||
| 137 | - | </td> | |||
| 138 | - | <td class="c-size num dim">12 B</td> | |||
| 139 | - | <td class="c-sub dim"> | |||
| 140 | - | <a href="/files/commit/SHA40">add src/lib/util.go</a> | |||
| 141 | - | </td> | |||
| 142 | - | <td class="c-who dim">T</td> | |||
| 143 | - | <td class="c-when nowrap dim"> | |||
| 144 | <time datetime="STAMP" title="STAMP">AGO</time> | 140 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 145 | - | </td> | 141 | + | </span> |
| 146 | - | </tr> | 142 | + | </li> |
| 147 | - | </tbody> | 143 | + | </ul> |
| 148 | - | </table> | |||
| 149 | </div> | 144 | </div> | ||
| 150 | </main> | 145 | </main> | ||
| 151 | </div> | 146 | </div> | ||
| @@ -194,14 +189,25 @@ | |||||
| 194 | <dt> | 189 | <dt> | ||
| 195 | <kbd>enter</kbd> | 190 | <kbd>enter</kbd> | ||
| 196 | </dt> | 191 | </dt> | ||
| 197 | - | <dd>open the row under the cursor</dd> | 192 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 193 | + | </div> | |||
| 194 | + | <div> | |||
| 195 | + | <dt> | |||
| 196 | + | <kbd>↑</kbd> | |||
| 197 | + | <kbd>↓</kbd> | |||
| 198 | + | <kbd>←</kbd> | |||
| 199 | + | <kbd>→</kbd> | |||
| 200 | + | <kbd>home</kbd> | |||
| 201 | + | <kbd>end</kbd> | |||
| 202 | + | </dt> | |||
| 203 | + | <dd>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</dd> | |||
| 198 | </div> | 204 | </div> | ||
| 199 | <div> | 205 | <div> | ||
| 200 | <dt> | 206 | <dt> | ||
| 201 | <kbd>l</kbd> | 207 | <kbd>l</kbd> | ||
| 202 | <kbd>h</kbd> | 208 | <kbd>h</kbd> | ||
| 203 | </dt> | 209 | </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> | 210 | + | <dd>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</dd> |
| 205 | </div> | 211 | </div> | ||
| 206 | <div> | 212 | <div> | ||
| 207 | <dt> | 213 | <dt> | ||
| @@ -214,7 +220,7 @@ | |||||
| 214 | <dt> | 220 | <dt> | ||
| 215 | <kbd>/</kbd> | 221 | <kbd>/</kbd> | ||
| 216 | </dt> | 222 | </dt> | ||
| 217 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 223 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 218 | </div> | 224 | </div> | ||
| 219 | <div> | 225 | <div> | ||
| 220 | <dt> | 226 | <dt> | ||
M internal/app/testdata/web/tree-root.html +258 -258
| @@ -96,283 +96,272 @@ | |||
| 96 | 96 | </header> | |
| 97 | 97 | <div class="work"> | |
| 98 | 98 | <main id="main" tabindex="-1"> | |
| 99 | - | <div class="filter"> | |
| 99 | + | <div class="tree-find"> | |
| 100 | + | <div class="filter wide"> | |
| 100 | 101 | <label for="finder-q" class="sr">find a file</label> | |
| 101 | - | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | |
| 102 | + | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file in this repository…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | |
| 102 | 103 | </div> | |
| 103 | - | <div id="finder" hidden> | |
| 104 | + | <p class="tree-keys" data-js aria-hidden="true"> | |
| 105 | + | <kbd>j</kbd> | |
| 106 | + | <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p> | |
| 107 | + | </div> | |
| 108 | + | <div id="finder" class="tree-card" hidden> | |
| 104 | 109 | <p id="finder-status" class="dim" aria-live="polite"> | |
| 105 | 110 | </p> | |
| 106 | 111 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | |
| 107 | 112 | </ul> | |
| 108 | 113 | </div> | |
| 109 | - | <div id="tree-view"> | |
| 110 | - | <table class="list tree" aria-label="Entries"> | |
| 111 | - | <thead> | |
| 112 | - | <tr> | |
| 113 | - | <th class="cur"> | |
| 114 | - | </th> | |
| 115 | - | <th class="c-kind"> | |
| 116 | - | </th> | |
| 117 | - | <th class="c-name">name</th> | |
| 118 | - | <th class="c-size">size</th> | |
| 119 | - | <th class="c-sub">last commit</th> | |
| 120 | - | <th class="c-who">author</th> | |
| 121 | - | <th class="c-when">date</th> | |
| 122 | - | </tr> | |
| 123 | - | </thead> | |
| 124 | - | <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)"> | |
| 125 | - | <tr data-cursor> | |
| 126 | - | <td class="cur" aria-hidden="true"> | |
| 127 | - | </td> | |
| 128 | - | <td class="c-kind" aria-hidden="true">▸</td> | |
| 129 | - | <td class="c-name"> | |
| 130 | - | <a href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false">docs/</a> | |
| 131 | - | </td> | |
| 132 | - | <td class="c-size num dim">-</td> | |
| 133 | - | <td class="c-sub dim"> | |
| 134 | - | <a href="/files/commit/SHA40">add docs/guide.md</a> | |
| 135 | - | </td> | |
| 136 | - | <td class="c-who dim">T</td> | |
| 137 | - | <td class="c-when nowrap dim"> | |
| 114 | + | <div id="tree-view" class="tree-card"> | |
| 115 | + | <div class="tree-head"> | |
| 116 | + | <span class="tree-path">/</span> | |
| 117 | + | <span class="tree-count">15 entries</span> | |
| 118 | + | </div> | |
| 119 | + | <ul id="tree-rows" class="tree" role="tree" aria-label="Entries" data-rows data-tree hx-on:click="ermiteTree.click(event)"> | |
| 120 | + | <li role="treeitem" aria-level="1" aria-posinset="1" aria-setsize="15" data-kind="dir" data-depth="0" data-cursor data-expand="/files/tree/main/docs?rows" aria-expanded="false"> | |
| 121 | + | <span class="t-main"> | |
| 122 | + | <span class="t-twig" aria-hidden="true"> | |
| 123 | + | </span> | |
| 124 | + | <span class="t-node" aria-hidden="true"> | |
| 125 | + | </span> | |
| 126 | + | <a class="t-name" href="/files/tree/main/docs" title="directory">docs/</a> | |
| 127 | + | <span class="sr"> (directory)</span> | |
| 128 | + | </span> | |
| 129 | + | <span class="t-size"> | |
| 130 | + | </span> | |
| 131 | + | <span class="t-sub"> | |
| 132 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add docs/guide.md</a> | |
| 133 | + | </span> | |
| 134 | + | <span class="t-when"> | |
| 138 | 135 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 139 | - | </td> | |
| 140 | - | </tr> | |
| 141 | - | <tr> | |
| 142 | - | <td class="cur" aria-hidden="true"> | |
| 143 | - | </td> | |
| 144 | - | <td class="c-kind" aria-hidden="true">▸</td> | |
| 145 | - | <td class="c-name"> | |
| 146 | - | <a href="/files/tree/main/src" data-expand="/files/tree/main/src?rows" aria-expanded="false">src/</a> | |
| 147 | - | </td> | |
| 148 | - | <td class="c-size num dim">-</td> | |
| 149 | - | <td class="c-sub dim"> | |
| 150 | - | <a href="/files/commit/SHA40">add src/lib/util.go</a> | |
| 151 | - | </td> | |
| 152 | - | <td class="c-who dim">T</td> | |
| 153 | - | <td class="c-when nowrap dim"> | |
| 136 | + | </span> | |
| 137 | + | </li> | |
| 138 | + | <li role="treeitem" aria-level="1" aria-posinset="2" aria-setsize="15" data-kind="dir" data-depth="0" data-expand="/files/tree/main/src?rows" aria-expanded="false"> | |
| 139 | + | <span class="t-main"> | |
| 140 | + | <span class="t-twig" aria-hidden="true"> | |
| 141 | + | </span> | |
| 142 | + | <span class="t-node" aria-hidden="true"> | |
| 143 | + | </span> | |
| 144 | + | <a class="t-name" href="/files/tree/main/src" title="directory">src/</a> | |
| 145 | + | <span class="sr"> (directory)</span> | |
| 146 | + | </span> | |
| 147 | + | <span class="t-size"> | |
| 148 | + | </span> | |
| 149 | + | <span class="t-sub"> | |
| 150 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add src/lib/util.go</a> | |
| 151 | + | </span> | |
| 152 | + | <span class="t-when"> | |
| 154 | 153 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 155 | - | </td> | |
| 156 | - | </tr> | |
| 157 | - | <tr> | |
| 158 | - | <td class="cur" aria-hidden="true"> | |
| 159 | - | </td> | |
| 160 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 161 | - | <td class="c-name"> | |
| 162 | - | <a href="/files/blob/main/README.md">README.md</a> | |
| 163 | - | </td> | |
| 164 | - | <td class="c-size num dim">41 B</td> | |
| 165 | - | <td class="c-sub dim"> | |
| 166 | - | <a href="/files/commit/SHA40">add README.md</a> | |
| 167 | - | </td> | |
| 168 | - | <td class="c-who dim">T</td> | |
| 169 | - | <td class="c-when nowrap dim"> | |
| 154 | + | </span> | |
| 155 | + | </li> | |
| 156 | + | <li role="treeitem" aria-level="1" aria-posinset="3" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 157 | + | <span class="t-main"> | |
| 158 | + | <span class="t-twig" aria-hidden="true"> | |
| 159 | + | </span> | |
| 160 | + | <span class="t-node" aria-hidden="true"> | |
| 161 | + | </span> | |
| 162 | + | <a class="t-name" href="/files/blob/main/README.md">README.md</a> | |
| 163 | + | </span> | |
| 164 | + | <span class="t-size">41 B</span> | |
| 165 | + | <span class="t-sub"> | |
| 166 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add README.md</a> | |
| 167 | + | </span> | |
| 168 | + | <span class="t-when"> | |
| 170 | 169 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 171 | - | </td> | |
| 172 | - | </tr> | |
| 173 | - | <tr> | |
| 174 | - | <td class="cur" aria-hidden="true"> | |
| 175 | - | </td> | |
| 176 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 177 | - | <td class="c-name"> | |
| 178 | - | <a href="/files/blob/main/big.txt">big.txt</a> | |
| 179 | - | </td> | |
| 180 | - | <td class="c-size num dim">1.1 MiB</td> | |
| 181 | - | <td class="c-sub dim"> | |
| 182 | - | <a href="/files/commit/SHA40">add big.txt</a> | |
| 183 | - | </td> | |
| 184 | - | <td class="c-who dim">T</td> | |
| 185 | - | <td class="c-when nowrap dim"> | |
| 170 | + | </span> | |
| 171 | + | </li> | |
| 172 | + | <li role="treeitem" aria-level="1" aria-posinset="4" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 173 | + | <span class="t-main"> | |
| 174 | + | <span class="t-twig" aria-hidden="true"> | |
| 175 | + | </span> | |
| 176 | + | <span class="t-node" aria-hidden="true"> | |
| 177 | + | </span> | |
| 178 | + | <a class="t-name" href="/files/blob/main/big.txt">big.txt</a> | |
| 179 | + | </span> | |
| 180 | + | <span class="t-size">1.1 MiB</span> | |
| 181 | + | <span class="t-sub"> | |
| 182 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add big.txt</a> | |
| 183 | + | </span> | |
| 184 | + | <span class="t-when"> | |
| 186 | 185 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 187 | - | </td> | |
| 188 | - | </tr> | |
| 189 | - | <tr> | |
| 190 | - | <td class="cur" aria-hidden="true"> | |
| 191 | - | </td> | |
| 192 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 193 | - | <td class="c-name"> | |
| 194 | - | <a href="/files/blob/main/data.bin">data.bin</a> | |
| 195 | - | </td> | |
| 196 | - | <td class="c-size num dim">10 B</td> | |
| 197 | - | <td class="c-sub dim"> | |
| 198 | - | <a href="/files/commit/SHA40">add data.bin</a> | |
| 199 | - | </td> | |
| 200 | - | <td class="c-who dim">T</td> | |
| 201 | - | <td class="c-when nowrap dim"> | |
| 186 | + | </span> | |
| 187 | + | </li> | |
| 188 | + | <li role="treeitem" aria-level="1" aria-posinset="5" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 189 | + | <span class="t-main"> | |
| 190 | + | <span class="t-twig" aria-hidden="true"> | |
| 191 | + | </span> | |
| 192 | + | <span class="t-node" aria-hidden="true"> | |
| 193 | + | </span> | |
| 194 | + | <a class="t-name" href="/files/blob/main/data.bin">data.bin</a> | |
| 195 | + | </span> | |
| 196 | + | <span class="t-size">10 B</span> | |
| 197 | + | <span class="t-sub"> | |
| 198 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add data.bin</a> | |
| 199 | + | </span> | |
| 200 | + | <span class="t-when"> | |
| 202 | 201 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 203 | - | </td> | |
| 204 | - | </tr> | |
| 205 | - | <tr> | |
| 206 | - | <td class="cur" aria-hidden="true"> | |
| 207 | - | </td> | |
| 208 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 209 | - | <td class="c-name"> | |
| 210 | - | <a href="/files/blob/main/empty.txt">empty.txt</a> | |
| 211 | - | </td> | |
| 212 | - | <td class="c-size num dim">0 B</td> | |
| 213 | - | <td class="c-sub dim"> | |
| 214 | - | <a href="/files/commit/SHA40">add empty.txt</a> | |
| 215 | - | </td> | |
| 216 | - | <td class="c-who dim">T</td> | |
| 217 | - | <td class="c-when nowrap dim"> | |
| 202 | + | </span> | |
| 203 | + | </li> | |
| 204 | + | <li role="treeitem" aria-level="1" aria-posinset="6" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 205 | + | <span class="t-main"> | |
| 206 | + | <span class="t-twig" aria-hidden="true"> | |
| 207 | + | </span> | |
| 208 | + | <span class="t-node" aria-hidden="true"> | |
| 209 | + | </span> | |
| 210 | + | <a class="t-name" href="/files/blob/main/empty.txt">empty.txt</a> | |
| 211 | + | </span> | |
| 212 | + | <span class="t-size">0 B</span> | |
| 213 | + | <span class="t-sub"> | |
| 214 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add empty.txt</a> | |
| 215 | + | </span> | |
| 216 | + | <span class="t-when"> | |
| 218 | 217 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 219 | - | </td> | |
| 220 | - | </tr> | |
| 221 | - | <tr> | |
| 222 | - | <td class="cur" aria-hidden="true"> | |
| 223 | - | </td> | |
| 224 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 225 | - | <td class="c-name"> | |
| 226 | - | <a href="/files/blob/main/evil.go">evil.go</a> | |
| 227 | - | </td> | |
| 228 | - | <td class="c-size num dim">87 B</td> | |
| 229 | - | <td class="c-sub dim"> | |
| 230 | - | <a href="/files/commit/SHA40">add evil.go</a> | |
| 231 | - | </td> | |
| 232 | - | <td class="c-who dim">T</td> | |
| 233 | - | <td class="c-when nowrap dim"> | |
| 218 | + | </span> | |
| 219 | + | </li> | |
| 220 | + | <li role="treeitem" aria-level="1" aria-posinset="7" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 221 | + | <span class="t-main"> | |
| 222 | + | <span class="t-twig" aria-hidden="true"> | |
| 223 | + | </span> | |
| 224 | + | <span class="t-node" aria-hidden="true"> | |
| 225 | + | </span> | |
| 226 | + | <a class="t-name" href="/files/blob/main/evil.go">evil.go</a> | |
| 227 | + | </span> | |
| 228 | + | <span class="t-size">87 B</span> | |
| 229 | + | <span class="t-sub"> | |
| 230 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add evil.go</a> | |
| 231 | + | </span> | |
| 232 | + | <span class="t-when"> | |
| 234 | 233 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 235 | - | </td> | |
| 236 | - | </tr> | |
| 237 | - | <tr> | |
| 238 | - | <td class="cur" aria-hidden="true"> | |
| 239 | - | </td> | |
| 240 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 241 | - | <td class="c-name"> | |
| 242 | - | <a href="/files/blob/main/fake.jpg">fake.jpg</a> | |
| 243 | - | </td> | |
| 244 | - | <td class="c-size num dim">15 B</td> | |
| 245 | - | <td class="c-sub dim"> | |
| 246 | - | <a href="/files/commit/SHA40">add fake.jpg</a> | |
| 247 | - | </td> | |
| 248 | - | <td class="c-who dim">T</td> | |
| 249 | - | <td class="c-when nowrap dim"> | |
| 234 | + | </span> | |
| 235 | + | </li> | |
| 236 | + | <li role="treeitem" aria-level="1" aria-posinset="8" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 237 | + | <span class="t-main"> | |
| 238 | + | <span class="t-twig" aria-hidden="true"> | |
| 239 | + | </span> | |
| 240 | + | <span class="t-node" aria-hidden="true"> | |
| 241 | + | </span> | |
| 242 | + | <a class="t-name" href="/files/blob/main/fake.jpg">fake.jpg</a> | |
| 243 | + | </span> | |
| 244 | + | <span class="t-size">15 B</span> | |
| 245 | + | <span class="t-sub"> | |
| 246 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add fake.jpg</a> | |
| 247 | + | </span> | |
| 248 | + | <span class="t-when"> | |
| 250 | 249 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 251 | - | </td> | |
| 252 | - | </tr> | |
| 253 | - | <tr> | |
| 254 | - | <td class="cur" aria-hidden="true"> | |
| 255 | - | </td> | |
| 256 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 257 | - | <td class="c-name"> | |
| 258 | - | <a href="/files/blob/main/fake.png">fake.png</a> | |
| 259 | - | </td> | |
| 260 | - | <td class="c-size num dim">39 B</td> | |
| 261 | - | <td class="c-sub dim"> | |
| 262 | - | <a href="/files/commit/SHA40">add fake.png</a> | |
| 263 | - | </td> | |
| 264 | - | <td class="c-who dim">T</td> | |
| 265 | - | <td class="c-when nowrap dim"> | |
| 250 | + | </span> | |
| 251 | + | </li> | |
| 252 | + | <li role="treeitem" aria-level="1" aria-posinset="9" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 253 | + | <span class="t-main"> | |
| 254 | + | <span class="t-twig" aria-hidden="true"> | |
| 255 | + | </span> | |
| 256 | + | <span class="t-node" aria-hidden="true"> | |
| 257 | + | </span> | |
| 258 | + | <a class="t-name" href="/files/blob/main/fake.png">fake.png</a> | |
| 259 | + | </span> | |
| 260 | + | <span class="t-size">39 B</span> | |
| 261 | + | <span class="t-sub"> | |
| 262 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add fake.png</a> | |
| 263 | + | </span> | |
| 264 | + | <span class="t-when"> | |
| 266 | 265 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 267 | - | </td> | |
| 268 | - | </tr> | |
| 269 | - | <tr> | |
| 270 | - | <td class="cur" aria-hidden="true"> | |
| 271 | - | </td> | |
| 272 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 273 | - | <td class="c-name"> | |
| 274 | - | <a href="/files/blob/main/lines.txt">lines.txt</a> | |
| 275 | - | </td> | |
| 276 | - | <td class="c-size num dim">39.1 KiB</td> | |
| 277 | - | <td class="c-sub dim"> | |
| 278 | - | <a href="/files/commit/SHA40">add lines.txt</a> | |
| 279 | - | </td> | |
| 280 | - | <td class="c-who dim">T</td> | |
| 281 | - | <td class="c-when nowrap dim"> | |
| 266 | + | </span> | |
| 267 | + | </li> | |
| 268 | + | <li role="treeitem" aria-level="1" aria-posinset="10" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 269 | + | <span class="t-main"> | |
| 270 | + | <span class="t-twig" aria-hidden="true"> | |
| 271 | + | </span> | |
| 272 | + | <span class="t-node" aria-hidden="true"> | |
| 273 | + | </span> | |
| 274 | + | <a class="t-name" href="/files/blob/main/lines.txt">lines.txt</a> | |
| 275 | + | </span> | |
| 276 | + | <span class="t-size">39.1 KiB</span> | |
| 277 | + | <span class="t-sub"> | |
| 278 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add lines.txt</a> | |
| 279 | + | </span> | |
| 280 | + | <span class="t-when"> | |
| 282 | 281 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 283 | - | </td> | |
| 284 | - | </tr> | |
| 285 | - | <tr> | |
| 286 | - | <td class="cur" aria-hidden="true"> | |
| 287 | - | </td> | |
| 288 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 289 | - | <td class="c-name"> | |
| 290 | - | <a href="/files/blob/main/logo.svg">logo.svg</a> | |
| 291 | - | </td> | |
| 292 | - | <td class="c-size num dim">89 B</td> | |
| 293 | - | <td class="c-sub dim"> | |
| 294 | - | <a href="/files/commit/SHA40">add logo.svg</a> | |
| 295 | - | </td> | |
| 296 | - | <td class="c-who dim">T</td> | |
| 297 | - | <td class="c-when nowrap dim"> | |
| 282 | + | </span> | |
| 283 | + | </li> | |
| 284 | + | <li role="treeitem" aria-level="1" aria-posinset="11" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 285 | + | <span class="t-main"> | |
| 286 | + | <span class="t-twig" aria-hidden="true"> | |
| 287 | + | </span> | |
| 288 | + | <span class="t-node" aria-hidden="true"> | |
| 289 | + | </span> | |
| 290 | + | <a class="t-name" href="/files/blob/main/logo.svg">logo.svg</a> | |
| 291 | + | </span> | |
| 292 | + | <span class="t-size">89 B</span> | |
| 293 | + | <span class="t-sub"> | |
| 294 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add logo.svg</a> | |
| 295 | + | </span> | |
| 296 | + | <span class="t-when"> | |
| 298 | 297 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 299 | - | </td> | |
| 300 | - | </tr> | |
| 301 | - | <tr> | |
| 302 | - | <td class="cur" aria-hidden="true"> | |
| 303 | - | </td> | |
| 304 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 305 | - | <td class="c-name"> | |
| 306 | - | <a href="/files/blob/main/main.go">main.go</a> | |
| 307 | - | </td> | |
| 308 | - | <td class="c-size num dim">87 B</td> | |
| 309 | - | <td class="c-sub dim"> | |
| 310 | - | <a href="/files/commit/SHA40">add main.go</a> | |
| 311 | - | </td> | |
| 312 | - | <td class="c-who dim">T</td> | |
| 313 | - | <td class="c-when nowrap dim"> | |
| 298 | + | </span> | |
| 299 | + | </li> | |
| 300 | + | <li role="treeitem" aria-level="1" aria-posinset="12" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 301 | + | <span class="t-main"> | |
| 302 | + | <span class="t-twig" aria-hidden="true"> | |
| 303 | + | </span> | |
| 304 | + | <span class="t-node" aria-hidden="true"> | |
| 305 | + | </span> | |
| 306 | + | <a class="t-name" href="/files/blob/main/main.go">main.go</a> | |
| 307 | + | </span> | |
| 308 | + | <span class="t-size">87 B</span> | |
| 309 | + | <span class="t-sub"> | |
| 310 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add main.go</a> | |
| 311 | + | </span> | |
| 312 | + | <span class="t-when"> | |
| 314 | 313 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 315 | - | </td> | |
| 316 | - | </tr> | |
| 317 | - | <tr> | |
| 318 | - | <td class="cur" aria-hidden="true"> | |
| 319 | - | </td> | |
| 320 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 321 | - | <td class="c-name"> | |
| 322 | - | <a href="/files/blob/main/notes.txt">notes.txt</a> | |
| 323 | - | </td> | |
| 324 | - | <td class="c-size num dim">24 B</td> | |
| 325 | - | <td class="c-sub dim"> | |
| 326 | - | <a href="/files/commit/SHA40">add notes.txt</a> | |
| 327 | - | </td> | |
| 328 | - | <td class="c-who dim">T</td> | |
| 329 | - | <td class="c-when nowrap dim"> | |
| 314 | + | </span> | |
| 315 | + | </li> | |
| 316 | + | <li role="treeitem" aria-level="1" aria-posinset="13" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 317 | + | <span class="t-main"> | |
| 318 | + | <span class="t-twig" aria-hidden="true"> | |
| 319 | + | </span> | |
| 320 | + | <span class="t-node" aria-hidden="true"> | |
| 321 | + | </span> | |
| 322 | + | <a class="t-name" href="/files/blob/main/notes.txt">notes.txt</a> | |
| 323 | + | </span> | |
| 324 | + | <span class="t-size">24 B</span> | |
| 325 | + | <span class="t-sub"> | |
| 326 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add notes.txt</a> | |
| 327 | + | </span> | |
| 328 | + | <span class="t-when"> | |
| 330 | 329 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 331 | - | </td> | |
| 332 | - | </tr> | |
| 333 | - | <tr> | |
| 334 | - | <td class="cur" aria-hidden="true"> | |
| 335 | - | </td> | |
| 336 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 337 | - | <td class="c-name"> | |
| 338 | - | <a href="/files/blob/main/page.html">page.html</a> | |
| 339 | - | </td> | |
| 340 | - | <td class="c-size num dim">26 B</td> | |
| 341 | - | <td class="c-sub dim"> | |
| 342 | - | <a href="/files/commit/SHA40">add page.html</a> | |
| 343 | - | </td> | |
| 344 | - | <td class="c-who dim">T</td> | |
| 345 | - | <td class="c-when nowrap dim"> | |
| 330 | + | </span> | |
| 331 | + | </li> | |
| 332 | + | <li role="treeitem" aria-level="1" aria-posinset="14" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 333 | + | <span class="t-main"> | |
| 334 | + | <span class="t-twig" aria-hidden="true"> | |
| 335 | + | </span> | |
| 336 | + | <span class="t-node" aria-hidden="true"> | |
| 337 | + | </span> | |
| 338 | + | <a class="t-name" href="/files/blob/main/page.html">page.html</a> | |
| 339 | + | </span> | |
| 340 | + | <span class="t-size">26 B</span> | |
| 341 | + | <span class="t-sub"> | |
| 342 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add page.html</a> | |
| 343 | + | </span> | |
| 344 | + | <span class="t-when"> | |
| 346 | 345 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 347 | - | </td> | |
| 348 | - | </tr> | |
| 349 | - | <tr> | |
| 350 | - | <td class="cur" aria-hidden="true"> | |
| 351 | - | </td> | |
| 352 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 353 | - | <td class="c-name"> | |
| 354 | - | <a href="/files/blob/main/pic.png">pic.png</a> | |
| 355 | - | </td> | |
| 356 | - | <td class="c-size num dim">88 B</td> | |
| 357 | - | <td class="c-sub dim"> | |
| 358 | - | <a href="/files/commit/SHA40">add pic.png</a> | |
| 359 | - | </td> | |
| 360 | - | <td class="c-who dim">T</td> | |
| 361 | - | <td class="c-when nowrap dim"> | |
| 346 | + | </span> | |
| 347 | + | </li> | |
| 348 | + | <li role="treeitem" aria-level="1" aria-posinset="15" aria-setsize="15" data-kind="file" data-depth="0"> | |
| 349 | + | <span class="t-main"> | |
| 350 | + | <span class="t-twig" aria-hidden="true"> | |
| 351 | + | </span> | |
| 352 | + | <span class="t-node" aria-hidden="true"> | |
| 353 | + | </span> | |
| 354 | + | <a class="t-name" href="/files/blob/main/pic.png">pic.png</a> | |
| 355 | + | </span> | |
| 356 | + | <span class="t-size">88 B</span> | |
| 357 | + | <span class="t-sub"> | |
| 358 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add pic.png</a> | |
| 359 | + | </span> | |
| 360 | + | <span class="t-when"> | |
| 362 | 361 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 363 | - | </td> | |
| 364 | - | </tr> | |
| 365 | - | </tbody> | |
| 366 | - | </table> | |
| 367 | - | <section class="card readme" aria-labelledby="readme-h"> | |
| 368 | - | <h2 class="card-h" id="readme-h"> | |
| 369 | - | <span class="ico i-file" aria-hidden="true"> | |
| 370 | - | </span> README.md</h2> | |
| 371 | - | <article class="prose" aria-label="README.md" hx-ignore> | |
| 372 | - | <h1 id="md-files">Files</h1> | |
| 373 | - | <p>See <a href="/files/blob/main/docs/guide.md">the guide</a>.</p> | |
| 374 | - | </article> | |
| 375 | - | </section> | |
| 362 | + | </span> | |
| 363 | + | </li> | |
| 364 | + | </ul> | |
| 376 | 365 | </div> | |
| 377 | 366 | </main> | |
| 378 | 367 | </div> | |
| @@ -421,14 +410,25 @@ | |||
| 421 | 410 | <dt> | |
| 422 | 411 | <kbd>enter</kbd> | |
| 423 | 412 | </dt> | |
| 424 | - | <dd>open the row under the cursor</dd> | |
| 413 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 414 | + | </div> | |
| 415 | + | <div> | |
| 416 | + | <dt> | |
| 417 | + | <kbd>↑</kbd> | |
| 418 | + | <kbd>↓</kbd> | |
| 419 | + | <kbd>←</kbd> | |
| 420 | + | <kbd>→</kbd> | |
| 421 | + | <kbd>home</kbd> | |
| 422 | + | <kbd>end</kbd> | |
| 423 | + | </dt> | |
| 424 | + | <dd>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</dd> | |
| 425 | 425 | </div> | |
| 426 | 426 | <div> | |
| 427 | 427 | <dt> | |
| 428 | 428 | <kbd>l</kbd> | |
| 429 | 429 | <kbd>h</kbd> | |
| 430 | 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> | |
| 431 | + | <dd>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</dd> | |
| 432 | 432 | </div> | |
| 433 | 433 | <div> | |
| 434 | 434 | <dt> | |
| @@ -441,7 +441,7 @@ | |||
| 441 | 441 | <dt> | |
| 442 | 442 | <kbd>/</kbd> | |
| 443 | 443 | </dt> | |
| 444 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 444 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 445 | 445 | </div> | |
| 446 | 446 | <div> | |
| 447 | 447 | <dt> | |
| @@ -96,283 +96,272 @@ | |||||
| 96 | </header> | 96 | </header> | ||
| 97 | <div class="work"> | 97 | <div class="work"> | ||
| 98 | <main id="main" tabindex="-1"> | 98 | <main id="main" tabindex="-1"> | ||
| 99 | - | <div class="filter"> | 99 | + | <div class="tree-find"> |
| 100 | + | <div class="filter wide"> | |||
| 100 | <label for="finder-q" class="sr">find a file</label> | 101 | <label for="finder-q" class="sr">find a file</label> | ||
| 101 | - | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | 102 | + | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file in this repository…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> |
| 102 | </div> | 103 | </div> | ||
| 103 | - | <div id="finder" hidden> | 104 | + | <p class="tree-keys" data-js aria-hidden="true"> |
| 105 | + | <kbd>j</kbd> | |||
| 106 | + | <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p> | |||
| 107 | + | </div> | |||
| 108 | + | <div id="finder" class="tree-card" hidden> | |||
| 104 | <p id="finder-status" class="dim" aria-live="polite"> | 109 | <p id="finder-status" class="dim" aria-live="polite"> | ||
| 105 | </p> | 110 | </p> | ||
| 106 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | 111 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | ||
| 107 | </ul> | 112 | </ul> | ||
| 108 | </div> | 113 | </div> | ||
| 109 | - | <div id="tree-view"> | 114 | + | <div id="tree-view" class="tree-card"> |
| 110 | - | <table class="list tree" aria-label="Entries"> | 115 | + | <div class="tree-head"> |
| 111 | - | <thead> | 116 | + | <span class="tree-path">/</span> |
| 112 | - | <tr> | 117 | + | <span class="tree-count">15 entries</span> |
| 113 | - | <th class="cur"> | 118 | + | </div> |
| 114 | - | </th> | 119 | + | <ul id="tree-rows" class="tree" role="tree" aria-label="Entries" data-rows data-tree hx-on:click="ermiteTree.click(event)"> |
| 115 | - | <th class="c-kind"> | 120 | + | <li role="treeitem" aria-level="1" aria-posinset="1" aria-setsize="15" data-kind="dir" data-depth="0" data-cursor data-expand="/files/tree/main/docs?rows" aria-expanded="false"> |
| 116 | - | </th> | 121 | + | <span class="t-main"> |
| 117 | - | <th class="c-name">name</th> | 122 | + | <span class="t-twig" aria-hidden="true"> |
| 118 | - | <th class="c-size">size</th> | 123 | + | </span> |
| 119 | - | <th class="c-sub">last commit</th> | 124 | + | <span class="t-node" aria-hidden="true"> |
| 120 | - | <th class="c-who">author</th> | 125 | + | </span> |
| 121 | - | <th class="c-when">date</th> | 126 | + | <a class="t-name" href="/files/tree/main/docs" title="directory">docs/</a> |
| 122 | - | </tr> | 127 | + | <span class="sr"> (directory)</span> |
| 123 | - | </thead> | 128 | + | </span> |
| 124 | - | <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)"> | 129 | + | <span class="t-size"> |
| 125 | - | <tr data-cursor> | 130 | + | </span> |
| 126 | - | <td class="cur" aria-hidden="true"> | 131 | + | <span class="t-sub"> |
| 127 | - | </td> | 132 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add docs/guide.md</a> |
| 128 | - | <td class="c-kind" aria-hidden="true">▸</td> | 133 | + | </span> |
| 129 | - | <td class="c-name"> | 134 | + | <span class="t-when"> |
| 130 | - | <a href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false">docs/</a> | |||
| 131 | - | </td> | |||
| 132 | - | <td class="c-size num dim">-</td> | |||
| 133 | - | <td class="c-sub dim"> | |||
| 134 | - | <a href="/files/commit/SHA40">add docs/guide.md</a> | |||
| 135 | - | </td> | |||
| 136 | - | <td class="c-who dim">T</td> | |||
| 137 | - | <td class="c-when nowrap dim"> | |||
| 138 | <time datetime="STAMP" title="STAMP">AGO</time> | 135 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 139 | - | </td> | 136 | + | </span> |
| 140 | - | </tr> | 137 | + | </li> |
| 141 | - | <tr> | 138 | + | <li role="treeitem" aria-level="1" aria-posinset="2" aria-setsize="15" data-kind="dir" data-depth="0" data-expand="/files/tree/main/src?rows" aria-expanded="false"> |
| 142 | - | <td class="cur" aria-hidden="true"> | 139 | + | <span class="t-main"> |
| 143 | - | </td> | 140 | + | <span class="t-twig" aria-hidden="true"> |
| 144 | - | <td class="c-kind" aria-hidden="true">▸</td> | 141 | + | </span> |
| 145 | - | <td class="c-name"> | 142 | + | <span class="t-node" aria-hidden="true"> |
| 146 | - | <a href="/files/tree/main/src" data-expand="/files/tree/main/src?rows" aria-expanded="false">src/</a> | 143 | + | </span> |
| 147 | - | </td> | 144 | + | <a class="t-name" href="/files/tree/main/src" title="directory">src/</a> |
| 148 | - | <td class="c-size num dim">-</td> | 145 | + | <span class="sr"> (directory)</span> |
| 149 | - | <td class="c-sub dim"> | 146 | + | </span> |
| 150 | - | <a href="/files/commit/SHA40">add src/lib/util.go</a> | 147 | + | <span class="t-size"> |
| 151 | - | </td> | 148 | + | </span> |
| 152 | - | <td class="c-who dim">T</td> | 149 | + | <span class="t-sub"> |
| 153 | - | <td class="c-when nowrap dim"> | 150 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add src/lib/util.go</a> |
| 151 | + | </span> | |||
| 152 | + | <span class="t-when"> | |||
| 154 | <time datetime="STAMP" title="STAMP">AGO</time> | 153 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 155 | - | </td> | 154 | + | </span> |
| 156 | - | </tr> | 155 | + | </li> |
| 157 | - | <tr> | 156 | + | <li role="treeitem" aria-level="1" aria-posinset="3" aria-setsize="15" data-kind="file" data-depth="0"> |
| 158 | - | <td class="cur" aria-hidden="true"> | 157 | + | <span class="t-main"> |
| 159 | - | </td> | 158 | + | <span class="t-twig" aria-hidden="true"> |
| 160 | - | <td class="c-kind" aria-hidden="true">·</td> | 159 | + | </span> |
| 161 | - | <td class="c-name"> | 160 | + | <span class="t-node" aria-hidden="true"> |
| 162 | - | <a href="/files/blob/main/README.md">README.md</a> | 161 | + | </span> |
| 163 | - | </td> | 162 | + | <a class="t-name" href="/files/blob/main/README.md">README.md</a> |
| 164 | - | <td class="c-size num dim">41 B</td> | 163 | + | </span> |
| 165 | - | <td class="c-sub dim"> | 164 | + | <span class="t-size">41 B</span> |
| 166 | - | <a href="/files/commit/SHA40">add README.md</a> | 165 | + | <span class="t-sub"> |
| 167 | - | </td> | 166 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add README.md</a> |
| 168 | - | <td class="c-who dim">T</td> | 167 | + | </span> |
| 169 | - | <td class="c-when nowrap dim"> | 168 | + | <span class="t-when"> |
| 170 | <time datetime="STAMP" title="STAMP">AGO</time> | 169 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 171 | - | </td> | 170 | + | </span> |
| 172 | - | </tr> | 171 | + | </li> |
| 173 | - | <tr> | 172 | + | <li role="treeitem" aria-level="1" aria-posinset="4" aria-setsize="15" data-kind="file" data-depth="0"> |
| 174 | - | <td class="cur" aria-hidden="true"> | 173 | + | <span class="t-main"> |
| 175 | - | </td> | 174 | + | <span class="t-twig" aria-hidden="true"> |
| 176 | - | <td class="c-kind" aria-hidden="true">·</td> | 175 | + | </span> |
| 177 | - | <td class="c-name"> | 176 | + | <span class="t-node" aria-hidden="true"> |
| 178 | - | <a href="/files/blob/main/big.txt">big.txt</a> | 177 | + | </span> |
| 179 | - | </td> | 178 | + | <a class="t-name" href="/files/blob/main/big.txt">big.txt</a> |
| 180 | - | <td class="c-size num dim">1.1 MiB</td> | 179 | + | </span> |
| 181 | - | <td class="c-sub dim"> | 180 | + | <span class="t-size">1.1 MiB</span> |
| 182 | - | <a href="/files/commit/SHA40">add big.txt</a> | 181 | + | <span class="t-sub"> |
| 183 | - | </td> | 182 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add big.txt</a> |
| 184 | - | <td class="c-who dim">T</td> | 183 | + | </span> |
| 185 | - | <td class="c-when nowrap dim"> | 184 | + | <span class="t-when"> |
| 186 | <time datetime="STAMP" title="STAMP">AGO</time> | 185 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 187 | - | </td> | 186 | + | </span> |
| 188 | - | </tr> | 187 | + | </li> |
| 189 | - | <tr> | 188 | + | <li role="treeitem" aria-level="1" aria-posinset="5" aria-setsize="15" data-kind="file" data-depth="0"> |
| 190 | - | <td class="cur" aria-hidden="true"> | 189 | + | <span class="t-main"> |
| 191 | - | </td> | 190 | + | <span class="t-twig" aria-hidden="true"> |
| 192 | - | <td class="c-kind" aria-hidden="true">·</td> | 191 | + | </span> |
| 193 | - | <td class="c-name"> | 192 | + | <span class="t-node" aria-hidden="true"> |
| 194 | - | <a href="/files/blob/main/data.bin">data.bin</a> | 193 | + | </span> |
| 195 | - | </td> | 194 | + | <a class="t-name" href="/files/blob/main/data.bin">data.bin</a> |
| 196 | - | <td class="c-size num dim">10 B</td> | 195 | + | </span> |
| 197 | - | <td class="c-sub dim"> | 196 | + | <span class="t-size">10 B</span> |
| 198 | - | <a href="/files/commit/SHA40">add data.bin</a> | 197 | + | <span class="t-sub"> |
| 199 | - | </td> | 198 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add data.bin</a> |
| 200 | - | <td class="c-who dim">T</td> | 199 | + | </span> |
| 201 | - | <td class="c-when nowrap dim"> | 200 | + | <span class="t-when"> |
| 202 | <time datetime="STAMP" title="STAMP">AGO</time> | 201 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 203 | - | </td> | 202 | + | </span> |
| 204 | - | </tr> | 203 | + | </li> |
| 205 | - | <tr> | 204 | + | <li role="treeitem" aria-level="1" aria-posinset="6" aria-setsize="15" data-kind="file" data-depth="0"> |
| 206 | - | <td class="cur" aria-hidden="true"> | 205 | + | <span class="t-main"> |
| 207 | - | </td> | 206 | + | <span class="t-twig" aria-hidden="true"> |
| 208 | - | <td class="c-kind" aria-hidden="true">·</td> | 207 | + | </span> |
| 209 | - | <td class="c-name"> | 208 | + | <span class="t-node" aria-hidden="true"> |
| 210 | - | <a href="/files/blob/main/empty.txt">empty.txt</a> | 209 | + | </span> |
| 211 | - | </td> | 210 | + | <a class="t-name" href="/files/blob/main/empty.txt">empty.txt</a> |
| 212 | - | <td class="c-size num dim">0 B</td> | 211 | + | </span> |
| 213 | - | <td class="c-sub dim"> | 212 | + | <span class="t-size">0 B</span> |
| 214 | - | <a href="/files/commit/SHA40">add empty.txt</a> | 213 | + | <span class="t-sub"> |
| 215 | - | </td> | 214 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add empty.txt</a> |
| 216 | - | <td class="c-who dim">T</td> | 215 | + | </span> |
| 217 | - | <td class="c-when nowrap dim"> | 216 | + | <span class="t-when"> |
| 218 | <time datetime="STAMP" title="STAMP">AGO</time> | 217 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 219 | - | </td> | 218 | + | </span> |
| 220 | - | </tr> | 219 | + | </li> |
| 221 | - | <tr> | 220 | + | <li role="treeitem" aria-level="1" aria-posinset="7" aria-setsize="15" data-kind="file" data-depth="0"> |
| 222 | - | <td class="cur" aria-hidden="true"> | 221 | + | <span class="t-main"> |
| 223 | - | </td> | 222 | + | <span class="t-twig" aria-hidden="true"> |
| 224 | - | <td class="c-kind" aria-hidden="true">·</td> | 223 | + | </span> |
| 225 | - | <td class="c-name"> | 224 | + | <span class="t-node" aria-hidden="true"> |
| 226 | - | <a href="/files/blob/main/evil.go">evil.go</a> | 225 | + | </span> |
| 227 | - | </td> | 226 | + | <a class="t-name" href="/files/blob/main/evil.go">evil.go</a> |
| 228 | - | <td class="c-size num dim">87 B</td> | 227 | + | </span> |
| 229 | - | <td class="c-sub dim"> | 228 | + | <span class="t-size">87 B</span> |
| 230 | - | <a href="/files/commit/SHA40">add evil.go</a> | 229 | + | <span class="t-sub"> |
| 231 | - | </td> | 230 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add evil.go</a> |
| 232 | - | <td class="c-who dim">T</td> | 231 | + | </span> |
| 233 | - | <td class="c-when nowrap dim"> | 232 | + | <span class="t-when"> |
| 234 | <time datetime="STAMP" title="STAMP">AGO</time> | 233 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 235 | - | </td> | 234 | + | </span> |
| 236 | - | </tr> | 235 | + | </li> |
| 237 | - | <tr> | 236 | + | <li role="treeitem" aria-level="1" aria-posinset="8" aria-setsize="15" data-kind="file" data-depth="0"> |
| 238 | - | <td class="cur" aria-hidden="true"> | 237 | + | <span class="t-main"> |
| 239 | - | </td> | 238 | + | <span class="t-twig" aria-hidden="true"> |
| 240 | - | <td class="c-kind" aria-hidden="true">·</td> | 239 | + | </span> |
| 241 | - | <td class="c-name"> | 240 | + | <span class="t-node" aria-hidden="true"> |
| 242 | - | <a href="/files/blob/main/fake.jpg">fake.jpg</a> | 241 | + | </span> |
| 243 | - | </td> | 242 | + | <a class="t-name" href="/files/blob/main/fake.jpg">fake.jpg</a> |
| 244 | - | <td class="c-size num dim">15 B</td> | 243 | + | </span> |
| 245 | - | <td class="c-sub dim"> | 244 | + | <span class="t-size">15 B</span> |
| 246 | - | <a href="/files/commit/SHA40">add fake.jpg</a> | 245 | + | <span class="t-sub"> |
| 247 | - | </td> | 246 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add fake.jpg</a> |
| 248 | - | <td class="c-who dim">T</td> | 247 | + | </span> |
| 249 | - | <td class="c-when nowrap dim"> | 248 | + | <span class="t-when"> |
| 250 | <time datetime="STAMP" title="STAMP">AGO</time> | 249 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 251 | - | </td> | 250 | + | </span> |
| 252 | - | </tr> | 251 | + | </li> |
| 253 | - | <tr> | 252 | + | <li role="treeitem" aria-level="1" aria-posinset="9" aria-setsize="15" data-kind="file" data-depth="0"> |
| 254 | - | <td class="cur" aria-hidden="true"> | 253 | + | <span class="t-main"> |
| 255 | - | </td> | 254 | + | <span class="t-twig" aria-hidden="true"> |
| 256 | - | <td class="c-kind" aria-hidden="true">·</td> | 255 | + | </span> |
| 257 | - | <td class="c-name"> | 256 | + | <span class="t-node" aria-hidden="true"> |
| 258 | - | <a href="/files/blob/main/fake.png">fake.png</a> | 257 | + | </span> |
| 259 | - | </td> | 258 | + | <a class="t-name" href="/files/blob/main/fake.png">fake.png</a> |
| 260 | - | <td class="c-size num dim">39 B</td> | 259 | + | </span> |
| 261 | - | <td class="c-sub dim"> | 260 | + | <span class="t-size">39 B</span> |
| 262 | - | <a href="/files/commit/SHA40">add fake.png</a> | 261 | + | <span class="t-sub"> |
| 263 | - | </td> | 262 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add fake.png</a> |
| 264 | - | <td class="c-who dim">T</td> | 263 | + | </span> |
| 265 | - | <td class="c-when nowrap dim"> | 264 | + | <span class="t-when"> |
| 266 | <time datetime="STAMP" title="STAMP">AGO</time> | 265 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 267 | - | </td> | 266 | + | </span> |
| 268 | - | </tr> | 267 | + | </li> |
| 269 | - | <tr> | 268 | + | <li role="treeitem" aria-level="1" aria-posinset="10" aria-setsize="15" data-kind="file" data-depth="0"> |
| 270 | - | <td class="cur" aria-hidden="true"> | 269 | + | <span class="t-main"> |
| 271 | - | </td> | 270 | + | <span class="t-twig" aria-hidden="true"> |
| 272 | - | <td class="c-kind" aria-hidden="true">·</td> | 271 | + | </span> |
| 273 | - | <td class="c-name"> | 272 | + | <span class="t-node" aria-hidden="true"> |
| 274 | - | <a href="/files/blob/main/lines.txt">lines.txt</a> | 273 | + | </span> |
| 275 | - | </td> | 274 | + | <a class="t-name" href="/files/blob/main/lines.txt">lines.txt</a> |
| 276 | - | <td class="c-size num dim">39.1 KiB</td> | 275 | + | </span> |
| 277 | - | <td class="c-sub dim"> | 276 | + | <span class="t-size">39.1 KiB</span> |
| 278 | - | <a href="/files/commit/SHA40">add lines.txt</a> | 277 | + | <span class="t-sub"> |
| 279 | - | </td> | 278 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add lines.txt</a> |
| 280 | - | <td class="c-who dim">T</td> | 279 | + | </span> |
| 281 | - | <td class="c-when nowrap dim"> | 280 | + | <span class="t-when"> |
| 282 | <time datetime="STAMP" title="STAMP">AGO</time> | 281 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 283 | - | </td> | 282 | + | </span> |
| 284 | - | </tr> | 283 | + | </li> |
| 285 | - | <tr> | 284 | + | <li role="treeitem" aria-level="1" aria-posinset="11" aria-setsize="15" data-kind="file" data-depth="0"> |
| 286 | - | <td class="cur" aria-hidden="true"> | 285 | + | <span class="t-main"> |
| 287 | - | </td> | 286 | + | <span class="t-twig" aria-hidden="true"> |
| 288 | - | <td class="c-kind" aria-hidden="true">·</td> | 287 | + | </span> |
| 289 | - | <td class="c-name"> | 288 | + | <span class="t-node" aria-hidden="true"> |
| 290 | - | <a href="/files/blob/main/logo.svg">logo.svg</a> | 289 | + | </span> |
| 291 | - | </td> | 290 | + | <a class="t-name" href="/files/blob/main/logo.svg">logo.svg</a> |
| 292 | - | <td class="c-size num dim">89 B</td> | 291 | + | </span> |
| 293 | - | <td class="c-sub dim"> | 292 | + | <span class="t-size">89 B</span> |
| 294 | - | <a href="/files/commit/SHA40">add logo.svg</a> | 293 | + | <span class="t-sub"> |
| 295 | - | </td> | 294 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add logo.svg</a> |
| 296 | - | <td class="c-who dim">T</td> | 295 | + | </span> |
| 297 | - | <td class="c-when nowrap dim"> | 296 | + | <span class="t-when"> |
| 298 | <time datetime="STAMP" title="STAMP">AGO</time> | 297 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 299 | - | </td> | 298 | + | </span> |
| 300 | - | </tr> | 299 | + | </li> |
| 301 | - | <tr> | 300 | + | <li role="treeitem" aria-level="1" aria-posinset="12" aria-setsize="15" data-kind="file" data-depth="0"> |
| 302 | - | <td class="cur" aria-hidden="true"> | 301 | + | <span class="t-main"> |
| 303 | - | </td> | 302 | + | <span class="t-twig" aria-hidden="true"> |
| 304 | - | <td class="c-kind" aria-hidden="true">·</td> | 303 | + | </span> |
| 305 | - | <td class="c-name"> | 304 | + | <span class="t-node" aria-hidden="true"> |
| 306 | - | <a href="/files/blob/main/main.go">main.go</a> | 305 | + | </span> |
| 307 | - | </td> | 306 | + | <a class="t-name" href="/files/blob/main/main.go">main.go</a> |
| 308 | - | <td class="c-size num dim">87 B</td> | 307 | + | </span> |
| 309 | - | <td class="c-sub dim"> | 308 | + | <span class="t-size">87 B</span> |
| 310 | - | <a href="/files/commit/SHA40">add main.go</a> | 309 | + | <span class="t-sub"> |
| 311 | - | </td> | 310 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add main.go</a> |
| 312 | - | <td class="c-who dim">T</td> | 311 | + | </span> |
| 313 | - | <td class="c-when nowrap dim"> | 312 | + | <span class="t-when"> |
| 314 | <time datetime="STAMP" title="STAMP">AGO</time> | 313 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 315 | - | </td> | 314 | + | </span> |
| 316 | - | </tr> | 315 | + | </li> |
| 317 | - | <tr> | 316 | + | <li role="treeitem" aria-level="1" aria-posinset="13" aria-setsize="15" data-kind="file" data-depth="0"> |
| 318 | - | <td class="cur" aria-hidden="true"> | 317 | + | <span class="t-main"> |
| 319 | - | </td> | 318 | + | <span class="t-twig" aria-hidden="true"> |
| 320 | - | <td class="c-kind" aria-hidden="true">·</td> | 319 | + | </span> |
| 321 | - | <td class="c-name"> | 320 | + | <span class="t-node" aria-hidden="true"> |
| 322 | - | <a href="/files/blob/main/notes.txt">notes.txt</a> | 321 | + | </span> |
| 323 | - | </td> | 322 | + | <a class="t-name" href="/files/blob/main/notes.txt">notes.txt</a> |
| 324 | - | <td class="c-size num dim">24 B</td> | 323 | + | </span> |
| 325 | - | <td class="c-sub dim"> | 324 | + | <span class="t-size">24 B</span> |
| 326 | - | <a href="/files/commit/SHA40">add notes.txt</a> | 325 | + | <span class="t-sub"> |
| 327 | - | </td> | 326 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add notes.txt</a> |
| 328 | - | <td class="c-who dim">T</td> | 327 | + | </span> |
| 329 | - | <td class="c-when nowrap dim"> | 328 | + | <span class="t-when"> |
| 330 | <time datetime="STAMP" title="STAMP">AGO</time> | 329 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 331 | - | </td> | 330 | + | </span> |
| 332 | - | </tr> | 331 | + | </li> |
| 333 | - | <tr> | 332 | + | <li role="treeitem" aria-level="1" aria-posinset="14" aria-setsize="15" data-kind="file" data-depth="0"> |
| 334 | - | <td class="cur" aria-hidden="true"> | 333 | + | <span class="t-main"> |
| 335 | - | </td> | 334 | + | <span class="t-twig" aria-hidden="true"> |
| 336 | - | <td class="c-kind" aria-hidden="true">·</td> | 335 | + | </span> |
| 337 | - | <td class="c-name"> | 336 | + | <span class="t-node" aria-hidden="true"> |
| 338 | - | <a href="/files/blob/main/page.html">page.html</a> | 337 | + | </span> |
| 339 | - | </td> | 338 | + | <a class="t-name" href="/files/blob/main/page.html">page.html</a> |
| 340 | - | <td class="c-size num dim">26 B</td> | 339 | + | </span> |
| 341 | - | <td class="c-sub dim"> | 340 | + | <span class="t-size">26 B</span> |
| 342 | - | <a href="/files/commit/SHA40">add page.html</a> | 341 | + | <span class="t-sub"> |
| 343 | - | </td> | 342 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add page.html</a> |
| 344 | - | <td class="c-who dim">T</td> | 343 | + | </span> |
| 345 | - | <td class="c-when nowrap dim"> | 344 | + | <span class="t-when"> |
| 346 | <time datetime="STAMP" title="STAMP">AGO</time> | 345 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 347 | - | </td> | 346 | + | </span> |
| 348 | - | </tr> | 347 | + | </li> |
| 349 | - | <tr> | 348 | + | <li role="treeitem" aria-level="1" aria-posinset="15" aria-setsize="15" data-kind="file" data-depth="0"> |
| 350 | - | <td class="cur" aria-hidden="true"> | 349 | + | <span class="t-main"> |
| 351 | - | </td> | 350 | + | <span class="t-twig" aria-hidden="true"> |
| 352 | - | <td class="c-kind" aria-hidden="true">·</td> | 351 | + | </span> |
| 353 | - | <td class="c-name"> | 352 | + | <span class="t-node" aria-hidden="true"> |
| 354 | - | <a href="/files/blob/main/pic.png">pic.png</a> | 353 | + | </span> |
| 355 | - | </td> | 354 | + | <a class="t-name" href="/files/blob/main/pic.png">pic.png</a> |
| 356 | - | <td class="c-size num dim">88 B</td> | 355 | + | </span> |
| 357 | - | <td class="c-sub dim"> | 356 | + | <span class="t-size">88 B</span> |
| 358 | - | <a href="/files/commit/SHA40">add pic.png</a> | 357 | + | <span class="t-sub"> |
| 359 | - | </td> | 358 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add pic.png</a> |
| 360 | - | <td class="c-who dim">T</td> | 359 | + | </span> |
| 361 | - | <td class="c-when nowrap dim"> | 360 | + | <span class="t-when"> |
| 362 | <time datetime="STAMP" title="STAMP">AGO</time> | 361 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 363 | - | </td> | 362 | + | </span> |
| 364 | - | </tr> | 363 | + | </li> |
| 365 | - | </tbody> | 364 | + | </ul> |
| 366 | - | </table> | |||
| 367 | - | <section class="card readme" aria-labelledby="readme-h"> | |||
| 368 | - | <h2 class="card-h" id="readme-h"> | |||
| 369 | - | <span class="ico i-file" aria-hidden="true"> | |||
| 370 | - | </span> README.md</h2> | |||
| 371 | - | <article class="prose" aria-label="README.md" hx-ignore> | |||
| 372 | - | <h1 id="md-files">Files</h1> | |||
| 373 | - | <p>See <a href="/files/blob/main/docs/guide.md">the guide</a>.</p> | |||
| 374 | - | </article> | |||
| 375 | - | </section> | |||
| 376 | </div> | 365 | </div> | ||
| 377 | </main> | 366 | </main> | ||
| 378 | </div> | 367 | </div> | ||
| @@ -421,14 +410,25 @@ | |||||
| 421 | <dt> | 410 | <dt> | ||
| 422 | <kbd>enter</kbd> | 411 | <kbd>enter</kbd> | ||
| 423 | </dt> | 412 | </dt> | ||
| 424 | - | <dd>open the row under the cursor</dd> | 413 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 414 | + | </div> | |||
| 415 | + | <div> | |||
| 416 | + | <dt> | |||
| 417 | + | <kbd>↑</kbd> | |||
| 418 | + | <kbd>↓</kbd> | |||
| 419 | + | <kbd>←</kbd> | |||
| 420 | + | <kbd>→</kbd> | |||
| 421 | + | <kbd>home</kbd> | |||
| 422 | + | <kbd>end</kbd> | |||
| 423 | + | </dt> | |||
| 424 | + | <dd>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</dd> | |||
| 425 | </div> | 425 | </div> | ||
| 426 | <div> | 426 | <div> | ||
| 427 | <dt> | 427 | <dt> | ||
| 428 | <kbd>l</kbd> | 428 | <kbd>l</kbd> | ||
| 429 | <kbd>h</kbd> | 429 | <kbd>h</kbd> | ||
| 430 | </dt> | 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> | 431 | + | <dd>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</dd> |
| 432 | </div> | 432 | </div> | ||
| 433 | <div> | 433 | <div> | ||
| 434 | <dt> | 434 | <dt> | ||
| @@ -441,7 +441,7 @@ | |||||
| 441 | <dt> | 441 | <dt> | ||
| 442 | <kbd>/</kbd> | 442 | <kbd>/</kbd> | ||
| 443 | </dt> | 443 | </dt> | ||
| 444 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 444 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 445 | </div> | 445 | </div> | ||
| 446 | <div> | 446 | <div> | ||
| 447 | <dt> | 447 | <dt> | ||
M internal/app/testdata/web/tree-slash-ref.html +45 -39
| @@ -99,50 +99,45 @@ | |||
| 99 | 99 | </header> | |
| 100 | 100 | <div class="work"> | |
| 101 | 101 | <main id="main" tabindex="-1"> | |
| 102 | - | <div class="filter"> | |
| 102 | + | <div class="tree-find"> | |
| 103 | + | <div class="filter wide"> | |
| 103 | 104 | <label for="finder-q" class="sr">find a file</label> | |
| 104 | - | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | |
| 105 | + | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file in this repository…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | |
| 105 | 106 | </div> | |
| 106 | - | <div id="finder" hidden> | |
| 107 | + | <p class="tree-keys" data-js aria-hidden="true"> | |
| 108 | + | <kbd>j</kbd> | |
| 109 | + | <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p> | |
| 110 | + | </div> | |
| 111 | + | <div id="finder" class="tree-card" hidden> | |
| 107 | 112 | <p id="finder-status" class="dim" aria-live="polite"> | |
| 108 | 113 | </p> | |
| 109 | 114 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | |
| 110 | 115 | </ul> | |
| 111 | 116 | </div> | |
| 112 | - | <div id="tree-view"> | |
| 113 | - | <table class="list tree" aria-label="Entries"> | |
| 114 | - | <thead> | |
| 115 | - | <tr> | |
| 116 | - | <th class="cur"> | |
| 117 | - | </th> | |
| 118 | - | <th class="c-kind"> | |
| 119 | - | </th> | |
| 120 | - | <th class="c-name">name</th> | |
| 121 | - | <th class="c-size">size</th> | |
| 122 | - | <th class="c-sub">last commit</th> | |
| 123 | - | <th class="c-who">author</th> | |
| 124 | - | <th class="c-when">date</th> | |
| 125 | - | </tr> | |
| 126 | - | </thead> | |
| 127 | - | <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)"> | |
| 128 | - | <tr data-cursor> | |
| 129 | - | <td class="cur" aria-hidden="true"> | |
| 130 | - | </td> | |
| 131 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 132 | - | <td class="c-name"> | |
| 133 | - | <a href="/files/blob/feature/x/x/y.txt">y.txt</a> | |
| 134 | - | </td> | |
| 135 | - | <td class="c-size num dim">22 B</td> | |
| 136 | - | <td class="c-sub dim"> | |
| 137 | - | <a href="/files/commit/SHA40">add x/y.txt</a> | |
| 138 | - | </td> | |
| 139 | - | <td class="c-who dim">T</td> | |
| 140 | - | <td class="c-when nowrap dim"> | |
| 117 | + | <div id="tree-view" class="tree-card"> | |
| 118 | + | <div class="tree-head"> | |
| 119 | + | <span class="tree-path">x/</span> | |
| 120 | + | <a class="tree-up" href="/files/tree/feature/x" title="parent directory (h)">..</a> | |
| 121 | + | <span class="tree-count">1 entry</span> | |
| 122 | + | </div> | |
| 123 | + | <ul id="tree-rows" class="tree" role="tree" aria-label="Entries" data-rows data-tree data-up="/files/tree/feature/x" hx-on:click="ermiteTree.click(event)"> | |
| 124 | + | <li role="treeitem" aria-level="1" aria-posinset="1" aria-setsize="1" data-kind="file" data-depth="0" data-cursor> | |
| 125 | + | <span class="t-main"> | |
| 126 | + | <span class="t-twig" aria-hidden="true"> | |
| 127 | + | </span> | |
| 128 | + | <span class="t-node" aria-hidden="true"> | |
| 129 | + | </span> | |
| 130 | + | <a class="t-name" href="/files/blob/feature/x/x/y.txt">y.txt</a> | |
| 131 | + | </span> | |
| 132 | + | <span class="t-size">22 B</span> | |
| 133 | + | <span class="t-sub"> | |
| 134 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add x/y.txt</a> | |
| 135 | + | </span> | |
| 136 | + | <span class="t-when"> | |
| 141 | 137 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 142 | - | </td> | |
| 143 | - | </tr> | |
| 144 | - | </tbody> | |
| 145 | - | </table> | |
| 138 | + | </span> | |
| 139 | + | </li> | |
| 140 | + | </ul> | |
| 146 | 141 | </div> | |
| 147 | 142 | </main> | |
| 148 | 143 | </div> | |
| @@ -191,14 +186,25 @@ | |||
| 191 | 186 | <dt> | |
| 192 | 187 | <kbd>enter</kbd> | |
| 193 | 188 | </dt> | |
| 194 | - | <dd>open the row under the cursor</dd> | |
| 189 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 190 | + | </div> | |
| 191 | + | <div> | |
| 192 | + | <dt> | |
| 193 | + | <kbd>↑</kbd> | |
| 194 | + | <kbd>↓</kbd> | |
| 195 | + | <kbd>←</kbd> | |
| 196 | + | <kbd>→</kbd> | |
| 197 | + | <kbd>home</kbd> | |
| 198 | + | <kbd>end</kbd> | |
| 199 | + | </dt> | |
| 200 | + | <dd>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</dd> | |
| 195 | 201 | </div> | |
| 196 | 202 | <div> | |
| 197 | 203 | <dt> | |
| 198 | 204 | <kbd>l</kbd> | |
| 199 | 205 | <kbd>h</kbd> | |
| 200 | 206 | </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> | |
| 207 | + | <dd>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</dd> | |
| 202 | 208 | </div> | |
| 203 | 209 | <div> | |
| 204 | 210 | <dt> | |
| @@ -211,7 +217,7 @@ | |||
| 211 | 217 | <dt> | |
| 212 | 218 | <kbd>/</kbd> | |
| 213 | 219 | </dt> | |
| 214 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 220 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 215 | 221 | </div> | |
| 216 | 222 | <div> | |
| 217 | 223 | <dt> | |
| @@ -99,50 +99,45 @@ | |||||
| 99 | </header> | 99 | </header> | ||
| 100 | <div class="work"> | 100 | <div class="work"> | ||
| 101 | <main id="main" tabindex="-1"> | 101 | <main id="main" tabindex="-1"> | ||
| 102 | - | <div class="filter"> | 102 | + | <div class="tree-find"> |
| 103 | + | <div class="filter wide"> | |||
| 103 | <label for="finder-q" class="sr">find a file</label> | 104 | <label for="finder-q" class="sr">find a file</label> | ||
| 104 | - | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | 105 | + | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file in this repository…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> |
| 105 | </div> | 106 | </div> | ||
| 106 | - | <div id="finder" hidden> | 107 | + | <p class="tree-keys" data-js aria-hidden="true"> |
| 108 | + | <kbd>j</kbd> | |||
| 109 | + | <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p> | |||
| 110 | + | </div> | |||
| 111 | + | <div id="finder" class="tree-card" hidden> | |||
| 107 | <p id="finder-status" class="dim" aria-live="polite"> | 112 | <p id="finder-status" class="dim" aria-live="polite"> | ||
| 108 | </p> | 113 | </p> | ||
| 109 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | 114 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | ||
| 110 | </ul> | 115 | </ul> | ||
| 111 | </div> | 116 | </div> | ||
| 112 | - | <div id="tree-view"> | 117 | + | <div id="tree-view" class="tree-card"> |
| 113 | - | <table class="list tree" aria-label="Entries"> | 118 | + | <div class="tree-head"> |
| 114 | - | <thead> | 119 | + | <span class="tree-path">x/</span> |
| 115 | - | <tr> | 120 | + | <a class="tree-up" href="/files/tree/feature/x" title="parent directory (h)">..</a> |
| 116 | - | <th class="cur"> | 121 | + | <span class="tree-count">1 entry</span> |
| 117 | - | </th> | 122 | + | </div> |
| 118 | - | <th class="c-kind"> | 123 | + | <ul id="tree-rows" class="tree" role="tree" aria-label="Entries" data-rows data-tree data-up="/files/tree/feature/x" hx-on:click="ermiteTree.click(event)"> |
| 119 | - | </th> | 124 | + | <li role="treeitem" aria-level="1" aria-posinset="1" aria-setsize="1" data-kind="file" data-depth="0" data-cursor> |
| 120 | - | <th class="c-name">name</th> | 125 | + | <span class="t-main"> |
| 121 | - | <th class="c-size">size</th> | 126 | + | <span class="t-twig" aria-hidden="true"> |
| 122 | - | <th class="c-sub">last commit</th> | 127 | + | </span> |
| 123 | - | <th class="c-who">author</th> | 128 | + | <span class="t-node" aria-hidden="true"> |
| 124 | - | <th class="c-when">date</th> | 129 | + | </span> |
| 125 | - | </tr> | 130 | + | <a class="t-name" href="/files/blob/feature/x/x/y.txt">y.txt</a> |
| 126 | - | </thead> | 131 | + | </span> |
| 127 | - | <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)"> | 132 | + | <span class="t-size">22 B</span> |
| 128 | - | <tr data-cursor> | 133 | + | <span class="t-sub"> |
| 129 | - | <td class="cur" aria-hidden="true"> | 134 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add x/y.txt</a> |
| 130 | - | </td> | 135 | + | </span> |
| 131 | - | <td class="c-kind" aria-hidden="true">·</td> | 136 | + | <span class="t-when"> |
| 132 | - | <td class="c-name"> | |||
| 133 | - | <a href="/files/blob/feature/x/x/y.txt">y.txt</a> | |||
| 134 | - | </td> | |||
| 135 | - | <td class="c-size num dim">22 B</td> | |||
| 136 | - | <td class="c-sub dim"> | |||
| 137 | - | <a href="/files/commit/SHA40">add x/y.txt</a> | |||
| 138 | - | </td> | |||
| 139 | - | <td class="c-who dim">T</td> | |||
| 140 | - | <td class="c-when nowrap dim"> | |||
| 141 | <time datetime="STAMP" title="STAMP">AGO</time> | 137 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 142 | - | </td> | 138 | + | </span> |
| 143 | - | </tr> | 139 | + | </li> |
| 144 | - | </tbody> | 140 | + | </ul> |
| 145 | - | </table> | |||
| 146 | </div> | 141 | </div> | ||
| 147 | </main> | 142 | </main> | ||
| 148 | </div> | 143 | </div> | ||
| @@ -191,14 +186,25 @@ | |||||
| 191 | <dt> | 186 | <dt> | ||
| 192 | <kbd>enter</kbd> | 187 | <kbd>enter</kbd> | ||
| 193 | </dt> | 188 | </dt> | ||
| 194 | - | <dd>open the row under the cursor</dd> | 189 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 190 | + | </div> | |||
| 191 | + | <div> | |||
| 192 | + | <dt> | |||
| 193 | + | <kbd>↑</kbd> | |||
| 194 | + | <kbd>↓</kbd> | |||
| 195 | + | <kbd>←</kbd> | |||
| 196 | + | <kbd>→</kbd> | |||
| 197 | + | <kbd>home</kbd> | |||
| 198 | + | <kbd>end</kbd> | |||
| 199 | + | </dt> | |||
| 200 | + | <dd>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</dd> | |||
| 195 | </div> | 201 | </div> | ||
| 196 | <div> | 202 | <div> | ||
| 197 | <dt> | 203 | <dt> | ||
| 198 | <kbd>l</kbd> | 204 | <kbd>l</kbd> | ||
| 199 | <kbd>h</kbd> | 205 | <kbd>h</kbd> | ||
| 200 | </dt> | 206 | </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> | 207 | + | <dd>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</dd> |
| 202 | </div> | 208 | </div> | ||
| 203 | <div> | 209 | <div> | ||
| 204 | <dt> | 210 | <dt> | ||
| @@ -211,7 +217,7 @@ | |||||
| 211 | <dt> | 217 | <dt> | ||
| 212 | <kbd>/</kbd> | 218 | <kbd>/</kbd> | ||
| 213 | </dt> | 219 | </dt> | ||
| 214 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 220 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 215 | </div> | 221 | </div> | ||
| 216 | <div> | 222 | <div> | ||
| 217 | <dt> | 223 | <dt> | ||
M internal/app/testdata/web/tree-subdir.html +60 -63
| @@ -99,75 +99,61 @@ | |||
| 99 | 99 | </header> | |
| 100 | 100 | <div class="work"> | |
| 101 | 101 | <main id="main" tabindex="-1"> | |
| 102 | - | <div class="filter"> | |
| 102 | + | <div class="tree-find"> | |
| 103 | + | <div class="filter wide"> | |
| 103 | 104 | <label for="finder-q" class="sr">find a file</label> | |
| 104 | - | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | |
| 105 | + | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file in this repository…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | |
| 105 | 106 | </div> | |
| 106 | - | <div id="finder" hidden> | |
| 107 | + | <p class="tree-keys" data-js aria-hidden="true"> | |
| 108 | + | <kbd>j</kbd> | |
| 109 | + | <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p> | |
| 110 | + | </div> | |
| 111 | + | <div id="finder" class="tree-card" hidden> | |
| 107 | 112 | <p id="finder-status" class="dim" aria-live="polite"> | |
| 108 | 113 | </p> | |
| 109 | 114 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | |
| 110 | 115 | </ul> | |
| 111 | 116 | </div> | |
| 112 | - | <div id="tree-view"> | |
| 113 | - | <table class="list tree" aria-label="Entries"> | |
| 114 | - | <thead> | |
| 115 | - | <tr> | |
| 116 | - | <th class="cur"> | |
| 117 | - | </th> | |
| 118 | - | <th class="c-kind"> | |
| 119 | - | </th> | |
| 120 | - | <th class="c-name">name</th> | |
| 121 | - | <th class="c-size">size</th> | |
| 122 | - | <th class="c-sub">last commit</th> | |
| 123 | - | <th class="c-who">author</th> | |
| 124 | - | <th class="c-when">date</th> | |
| 125 | - | </tr> | |
| 126 | - | </thead> | |
| 127 | - | <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)"> | |
| 128 | - | <tr data-cursor> | |
| 129 | - | <td class="cur" aria-hidden="true"> | |
| 130 | - | </td> | |
| 131 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 132 | - | <td class="c-name"> | |
| 133 | - | <a href="/files/blob/main/docs/README.md">README.md</a> | |
| 134 | - | </td> | |
| 135 | - | <td class="c-size num dim">62 B</td> | |
| 136 | - | <td class="c-sub dim"> | |
| 137 | - | <a href="/files/commit/SHA40">add docs/README.md</a> | |
| 138 | - | </td> | |
| 139 | - | <td class="c-who dim">T</td> | |
| 140 | - | <td class="c-when nowrap dim"> | |
| 117 | + | <div id="tree-view" class="tree-card"> | |
| 118 | + | <div class="tree-head"> | |
| 119 | + | <span class="tree-path">docs/</span> | |
| 120 | + | <a class="tree-up" href="/files/tree/main" title="parent directory (h)">..</a> | |
| 121 | + | <span class="tree-count">2 entries</span> | |
| 122 | + | </div> | |
| 123 | + | <ul id="tree-rows" class="tree" role="tree" aria-label="Entries" data-rows data-tree data-up="/files/tree/main" hx-on:click="ermiteTree.click(event)"> | |
| 124 | + | <li role="treeitem" aria-level="1" aria-posinset="1" aria-setsize="2" data-kind="file" data-depth="0" data-cursor> | |
| 125 | + | <span class="t-main"> | |
| 126 | + | <span class="t-twig" aria-hidden="true"> | |
| 127 | + | </span> | |
| 128 | + | <span class="t-node" aria-hidden="true"> | |
| 129 | + | </span> | |
| 130 | + | <a class="t-name" href="/files/blob/main/docs/README.md">README.md</a> | |
| 131 | + | </span> | |
| 132 | + | <span class="t-size">62 B</span> | |
| 133 | + | <span class="t-sub"> | |
| 134 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add docs/README.md</a> | |
| 135 | + | </span> | |
| 136 | + | <span class="t-when"> | |
| 141 | 137 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 142 | - | </td> | |
| 143 | - | </tr> | |
| 144 | - | <tr> | |
| 145 | - | <td class="cur" aria-hidden="true"> | |
| 146 | - | </td> | |
| 147 | - | <td class="c-kind" aria-hidden="true">·</td> | |
| 148 | - | <td class="c-name"> | |
| 149 | - | <a href="/files/blob/main/docs/guide.md">guide.md</a> | |
| 150 | - | </td> | |
| 151 | - | <td class="c-size num dim">47 B</td> | |
| 152 | - | <td class="c-sub dim"> | |
| 153 | - | <a href="/files/commit/SHA40">add docs/guide.md</a> | |
| 154 | - | </td> | |
| 155 | - | <td class="c-who dim">T</td> | |
| 156 | - | <td class="c-when nowrap dim"> | |
| 138 | + | </span> | |
| 139 | + | </li> | |
| 140 | + | <li role="treeitem" aria-level="1" aria-posinset="2" aria-setsize="2" data-kind="file" data-depth="0"> | |
| 141 | + | <span class="t-main"> | |
| 142 | + | <span class="t-twig" aria-hidden="true"> | |
| 143 | + | </span> | |
| 144 | + | <span class="t-node" aria-hidden="true"> | |
| 145 | + | </span> | |
| 146 | + | <a class="t-name" href="/files/blob/main/docs/guide.md">guide.md</a> | |
| 147 | + | </span> | |
| 148 | + | <span class="t-size">47 B</span> | |
| 149 | + | <span class="t-sub"> | |
| 150 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add docs/guide.md</a> | |
| 151 | + | </span> | |
| 152 | + | <span class="t-when"> | |
| 157 | 153 | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 158 | - | </td> | |
| 159 | - | </tr> | |
| 160 | - | </tbody> | |
| 161 | - | </table> | |
| 162 | - | <section class="card readme" aria-labelledby="readme-h"> | |
| 163 | - | <h2 class="card-h" id="readme-h"> | |
| 164 | - | <span class="ico i-file" aria-hidden="true"> | |
| 165 | - | </span> README.md</h2> | |
| 166 | - | <article class="prose" aria-label="README.md" hx-ignore> | |
| 167 | - | <h1 id="md-docs">Docs</h1> | |
| 168 | - | <p>Back to <a href="/files/blob/main/docs/guide.md">the guide</a> and <a href="/files/blob/main/main.go">root</a>.</p> | |
| 169 | - | </article> | |
| 170 | - | </section> | |
| 154 | + | </span> | |
| 155 | + | </li> | |
| 156 | + | </ul> | |
| 171 | 157 | </div> | |
| 172 | 158 | </main> | |
| 173 | 159 | </div> | |
| @@ -216,14 +202,25 @@ | |||
| 216 | 202 | <dt> | |
| 217 | 203 | <kbd>enter</kbd> | |
| 218 | 204 | </dt> | |
| 219 | - | <dd>open the row under the cursor</dd> | |
| 205 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> | |
| 206 | + | </div> | |
| 207 | + | <div> | |
| 208 | + | <dt> | |
| 209 | + | <kbd>↑</kbd> | |
| 210 | + | <kbd>↓</kbd> | |
| 211 | + | <kbd>←</kbd> | |
| 212 | + | <kbd>→</kbd> | |
| 213 | + | <kbd>home</kbd> | |
| 214 | + | <kbd>end</kbd> | |
| 215 | + | </dt> | |
| 216 | + | <dd>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</dd> | |
| 220 | 217 | </div> | |
| 221 | 218 | <div> | |
| 222 | 219 | <dt> | |
| 223 | 220 | <kbd>l</kbd> | |
| 224 | 221 | <kbd>h</kbd> | |
| 225 | 222 | </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> | |
| 223 | + | <dd>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</dd> | |
| 227 | 224 | </div> | |
| 228 | 225 | <div> | |
| 229 | 226 | <dt> | |
| @@ -236,7 +233,7 @@ | |||
| 236 | 233 | <dt> | |
| 237 | 234 | <kbd>/</kbd> | |
| 238 | 235 | </dt> | |
| 239 | - | <dd>filter the list; on Files, find a file in the repository</dd> | |
| 236 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> | |
| 240 | 237 | </div> | |
| 241 | 238 | <div> | |
| 242 | 239 | <dt> | |
| @@ -99,75 +99,61 @@ | |||||
| 99 | </header> | 99 | </header> | ||
| 100 | <div class="work"> | 100 | <div class="work"> | ||
| 101 | <main id="main" tabindex="-1"> | 101 | <main id="main" tabindex="-1"> | ||
| 102 | - | <div class="filter"> | 102 | + | <div class="tree-find"> |
| 103 | + | <div class="filter wide"> | |||
| 103 | <label for="finder-q" class="sr">find a file</label> | 104 | <label for="finder-q" class="sr">find a file</label> | ||
| 104 | - | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> | 105 | + | <input id="finder-q" type="search" data-filter role="combobox" aria-autocomplete="list" aria-controls="finder-list" aria-expanded="false" placeholder="Find a file in this repository…" autocomplete="off" spellcheck="false" hx-on:focus="ermiteFinder.load()" hx-on:input="ermiteFinder.input(this)" hx-on:keydown="ermiteFinder.key(this, event)"> |
| 105 | </div> | 106 | </div> | ||
| 106 | - | <div id="finder" hidden> | 107 | + | <p class="tree-keys" data-js aria-hidden="true"> |
| 108 | + | <kbd>j</kbd> | |||
| 109 | + | <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p> | |||
| 110 | + | </div> | |||
| 111 | + | <div id="finder" class="tree-card" hidden> | |||
| 107 | <p id="finder-status" class="dim" aria-live="polite"> | 112 | <p id="finder-status" class="dim" aria-live="polite"> | ||
| 108 | </p> | 113 | </p> | ||
| 109 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | 114 | <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"> | ||
| 110 | </ul> | 115 | </ul> | ||
| 111 | </div> | 116 | </div> | ||
| 112 | - | <div id="tree-view"> | 117 | + | <div id="tree-view" class="tree-card"> |
| 113 | - | <table class="list tree" aria-label="Entries"> | 118 | + | <div class="tree-head"> |
| 114 | - | <thead> | 119 | + | <span class="tree-path">docs/</span> |
| 115 | - | <tr> | 120 | + | <a class="tree-up" href="/files/tree/main" title="parent directory (h)">..</a> |
| 116 | - | <th class="cur"> | 121 | + | <span class="tree-count">2 entries</span> |
| 117 | - | </th> | 122 | + | </div> |
| 118 | - | <th class="c-kind"> | 123 | + | <ul id="tree-rows" class="tree" role="tree" aria-label="Entries" data-rows data-tree data-up="/files/tree/main" hx-on:click="ermiteTree.click(event)"> |
| 119 | - | </th> | 124 | + | <li role="treeitem" aria-level="1" aria-posinset="1" aria-setsize="2" data-kind="file" data-depth="0" data-cursor> |
| 120 | - | <th class="c-name">name</th> | 125 | + | <span class="t-main"> |
| 121 | - | <th class="c-size">size</th> | 126 | + | <span class="t-twig" aria-hidden="true"> |
| 122 | - | <th class="c-sub">last commit</th> | 127 | + | </span> |
| 123 | - | <th class="c-who">author</th> | 128 | + | <span class="t-node" aria-hidden="true"> |
| 124 | - | <th class="c-when">date</th> | 129 | + | </span> |
| 125 | - | </tr> | 130 | + | <a class="t-name" href="/files/blob/main/docs/README.md">README.md</a> |
| 126 | - | </thead> | 131 | + | </span> |
| 127 | - | <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)"> | 132 | + | <span class="t-size">62 B</span> |
| 128 | - | <tr data-cursor> | 133 | + | <span class="t-sub"> |
| 129 | - | <td class="cur" aria-hidden="true"> | 134 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add docs/README.md</a> |
| 130 | - | </td> | 135 | + | </span> |
| 131 | - | <td class="c-kind" aria-hidden="true">·</td> | 136 | + | <span class="t-when"> |
| 132 | - | <td class="c-name"> | |||
| 133 | - | <a href="/files/blob/main/docs/README.md">README.md</a> | |||
| 134 | - | </td> | |||
| 135 | - | <td class="c-size num dim">62 B</td> | |||
| 136 | - | <td class="c-sub dim"> | |||
| 137 | - | <a href="/files/commit/SHA40">add docs/README.md</a> | |||
| 138 | - | </td> | |||
| 139 | - | <td class="c-who dim">T</td> | |||
| 140 | - | <td class="c-when nowrap dim"> | |||
| 141 | <time datetime="STAMP" title="STAMP">AGO</time> | 137 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 142 | - | </td> | 138 | + | </span> |
| 143 | - | </tr> | 139 | + | </li> |
| 144 | - | <tr> | 140 | + | <li role="treeitem" aria-level="1" aria-posinset="2" aria-setsize="2" data-kind="file" data-depth="0"> |
| 145 | - | <td class="cur" aria-hidden="true"> | 141 | + | <span class="t-main"> |
| 146 | - | </td> | 142 | + | <span class="t-twig" aria-hidden="true"> |
| 147 | - | <td class="c-kind" aria-hidden="true">·</td> | 143 | + | </span> |
| 148 | - | <td class="c-name"> | 144 | + | <span class="t-node" aria-hidden="true"> |
| 149 | - | <a href="/files/blob/main/docs/guide.md">guide.md</a> | 145 | + | </span> |
| 150 | - | </td> | 146 | + | <a class="t-name" href="/files/blob/main/docs/guide.md">guide.md</a> |
| 151 | - | <td class="c-size num dim">47 B</td> | 147 | + | </span> |
| 152 | - | <td class="c-sub dim"> | 148 | + | <span class="t-size">47 B</span> |
| 153 | - | <a href="/files/commit/SHA40">add docs/guide.md</a> | 149 | + | <span class="t-sub"> |
| 154 | - | </td> | 150 | + | <a class="t-commit" href="/files/commit/SHA40" title="T">add docs/guide.md</a> |
| 155 | - | <td class="c-who dim">T</td> | 151 | + | </span> |
| 156 | - | <td class="c-when nowrap dim"> | 152 | + | <span class="t-when"> |
| 157 | <time datetime="STAMP" title="STAMP">AGO</time> | 153 | <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 158 | - | </td> | 154 | + | </span> |
| 159 | - | </tr> | 155 | + | </li> |
| 160 | - | </tbody> | 156 | + | </ul> |
| 161 | - | </table> | |||
| 162 | - | <section class="card readme" aria-labelledby="readme-h"> | |||
| 163 | - | <h2 class="card-h" id="readme-h"> | |||
| 164 | - | <span class="ico i-file" aria-hidden="true"> | |||
| 165 | - | </span> README.md</h2> | |||
| 166 | - | <article class="prose" aria-label="README.md" hx-ignore> | |||
| 167 | - | <h1 id="md-docs">Docs</h1> | |||
| 168 | - | <p>Back to <a href="/files/blob/main/docs/guide.md">the guide</a> and <a href="/files/blob/main/main.go">root</a>.</p> | |||
| 169 | - | </article> | |||
| 170 | - | </section> | |||
| 171 | </div> | 157 | </div> | ||
| 172 | </main> | 158 | </main> | ||
| 173 | </div> | 159 | </div> | ||
| @@ -216,14 +202,25 @@ | |||||
| 216 | <dt> | 202 | <dt> | ||
| 217 | <kbd>enter</kbd> | 203 | <kbd>enter</kbd> | ||
| 218 | </dt> | 204 | </dt> | ||
| 219 | - | <dd>open the row under the cursor</dd> | 205 | + | <dd>open the row under the cursor; on Files, a directory opens or closes in place</dd> |
| 206 | + | </div> | |||
| 207 | + | <div> | |||
| 208 | + | <dt> | |||
| 209 | + | <kbd>↑</kbd> | |||
| 210 | + | <kbd>↓</kbd> | |||
| 211 | + | <kbd>←</kbd> | |||
| 212 | + | <kbd>→</kbd> | |||
| 213 | + | <kbd>home</kbd> | |||
| 214 | + | <kbd>end</kbd> | |||
| 215 | + | </dt> | |||
| 216 | + | <dd>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</dd> | |||
| 220 | </div> | 217 | </div> | ||
| 221 | <div> | 218 | <div> | ||
| 222 | <dt> | 219 | <dt> | ||
| 223 | <kbd>l</kbd> | 220 | <kbd>l</kbd> | ||
| 224 | <kbd>h</kbd> | 221 | <kbd>h</kbd> | ||
| 225 | </dt> | 222 | </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> | 223 | + | <dd>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</dd> |
| 227 | </div> | 224 | </div> | ||
| 228 | <div> | 225 | <div> | ||
| 229 | <dt> | 226 | <dt> | ||
| @@ -236,7 +233,7 @@ | |||||
| 236 | <dt> | 233 | <dt> | ||
| 237 | <kbd>/</kbd> | 234 | <kbd>/</kbd> | ||
| 238 | </dt> | 235 | </dt> | ||
| 239 | - | <dd>filter the list; on Files, find a file in the repository</dd> | 236 | + | <dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd> |
| 240 | </div> | 237 | </div> | ||
| 241 | <div> | 238 | <div> | ||
| 242 | <dt> | 239 | <dt> | ||
M internal/app/web_files_test.go +80 -11
| @@ -116,7 +116,15 @@ func TestWebTreeRows(t *testing.T) { | |||
| 116 | 116 | ||
| 117 | 117 | // The page links its directories to their rows. | |
| 118 | 118 | _, _, page := h.get("/files/tree/main") | |
| 119 | - | assert.Contains(t, page, `href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false"`) | |
| 119 | + | assert.Contains(t, page, `data-expand="/files/tree/main/docs?rows" aria-expanded="false"`) | |
| 120 | + | assert.Contains(t, page, `<a class="t-name" href="/files/tree/main/docs" title="directory">docs/</a>`) | |
| 121 | + | // Every row says its place among the entries of its own directory. | |
| 122 | + | assert.Contains(t, page, `aria-level="1" aria-posinset="1" aria-setsize="15" data-kind="dir"`) | |
| 123 | + | assert.Contains(t, page, `aria-posinset="15" aria-setsize="15" data-kind="file"`) | |
| 124 | + | assert.Equal(t, 15, strings.Count(page, `role="treeitem"`)) | |
| 125 | + | assert.Contains(t, page, `<a class="t-commit" href="/files/commit/`) | |
| 126 | + | assert.NotContains(t, page, "<h1", "no README below the tree: the summary shows it") | |
| 127 | + | assert.NotContains(t, page, "See the guide") | |
| 120 | 128 | assert.Contains(t, page, `<script src="/-/static/js/files.`) | |
| 121 | 129 | assert.NotContains(t, page, "main.go</span>", "the palette lists no file") | |
| 122 | 130 | ||
| @@ -126,16 +134,19 @@ func TestWebTreeRows(t *testing.T) { | |||
| 126 | 134 | assert.Contains(t, hdr.Get("Content-Type"), "text/html") | |
| 127 | 135 | assert.NotContains(t, body, "<html") | |
| 128 | 136 | assert.NotContains(t, body, "<table") | |
| 129 | - | assert.Equal(t, 2, strings.Count(body, "<tr"), "one row per entry") | |
| 130 | - | assert.Contains(t, body, `<a href="/files/blob/main/docs/README.md">README.md</a>`) | |
| 131 | - | assert.Contains(t, body, `<a href="/files/blob/main/docs/guide.md">guide.md</a>`) | |
| 137 | + | assert.NotContains(t, body, "<ul") | |
| 138 | + | assert.Equal(t, 2, strings.Count(body, "<li"), "one row per entry") | |
| 139 | + | assert.Contains(t, body, `aria-posinset="1" aria-setsize="2"`) | |
| 140 | + | assert.Contains(t, body, `aria-posinset="2" aria-setsize="2"`, "positions count the entries of the directory itself") | |
| 141 | + | assert.Contains(t, body, `<a class="t-name" href="/files/blob/main/docs/README.md">README.md</a>`) | |
| 142 | + | assert.Contains(t, body, `<a class="t-name" href="/files/blob/main/docs/guide.md">guide.md</a>`) | |
| 132 | 143 | assert.NotContains(t, body, "data-cursor", "the cursor stays on the page") | |
| 133 | 144 | assertNoInline(t, body) | |
| 134 | 145 | ||
| 135 | 146 | // A subdirectory row can be opened in turn. | |
| 136 | 147 | _, _, body = h.get("/files/tree/main/src?rows") | |
| 137 | - | assert.Contains(t, body, `href="/files/tree/main/src/lib" data-expand="/files/tree/main/src/lib?rows" aria-expanded="false"`) | |
| 138 | - | assert.Contains(t, body, "▸") | |
| 148 | + | assert.Contains(t, body, `data-expand="/files/tree/main/src/lib?rows" aria-expanded="false"`) | |
| 149 | + | assert.Contains(t, body, `data-kind="dir"`) | |
| 139 | 150 | _, _, body = h.get("/files/tree/main/src/lib?rows") | |
| 140 | 151 | assert.Contains(t, body, `href="/files/blob/main/src/lib/util.go"`) | |
| 141 | 152 | assert.NotContains(t, body, "data-expand") | |
| @@ -169,13 +180,68 @@ func TestWebTreeRowsTruncated(t *testing.T) { | |||
| 169 | 180 | ||
| 170 | 181 | status, _, body := h.get("/wide/tree/main/many?rows") | |
| 171 | 182 | require.Equal(t, 200, status) | |
| 172 | - | assert.Equal(t, domain.MaxTreeEntries+1, strings.Count(body, "<tr"), "the listed entries and the row that says there are more") | |
| 173 | - | assert.Contains(t, body, `<tr class="tree-more"><td colspan="7"><a href="/wide/tree/main/many">more entries: open the directory</a></td></tr>`) | |
| 183 | + | assert.Equal(t, domain.MaxTreeEntries+1, strings.Count(body, "<li"), "the listed entries and the row that says there are more") | |
| 184 | + | assert.Contains(t, body, `<a class="t-name" href="/wide/tree/main/many">more entries: open the directory</a>`) | |
| 174 | 185 | assert.Equal(t, 1, strings.Count(body, "tree-more")) | |
| 186 | + | // The listing is cut: the size is unknown (-1), and the row that says so is the next position. | |
| 187 | + | assert.Equal(t, domain.MaxTreeEntries, strings.Count(body, `aria-setsize="-1" data-kind=`)) | |
| 188 | + | assert.Contains(t, body, `aria-posinset="1" aria-setsize="-1" data-kind="file"`) | |
| 189 | + | assert.Contains(t, body, fmt.Sprintf(`class="tree-more" role="treeitem" aria-level="1" aria-posinset="%d" aria-setsize="-1"`, domain.MaxTreeEntries+1)) | |
| 175 | 190 | ||
| 176 | 191 | // A directory that fits has no such row. | |
| 177 | 192 | _, _, body = h.get("/wide/tree?rows") | |
| 178 | 193 | assert.NotContains(t, body, "tree-more") | |
| 194 | + | ||
| 195 | + | // The page of the directory says the listing is cut, in a note, and its | |
| 196 | + | // rows are of unknown number. | |
| 197 | + | status, _, page := h.get("/wide/tree/main/many") | |
| 198 | + | require.Equal(t, 200, status) | |
| 199 | + | assert.Contains(t, page, fmt.Sprintf(`<p class="note">this directory has more entries than are listed here: the first %d are shown.</p>`, domain.MaxTreeEntries)) | |
| 200 | + | assert.Contains(t, page, `aria-posinset="1" aria-setsize="-1" data-kind="file"`) | |
| 201 | + | assert.NotContains(t, page, "tree-more", "the page of the directory has no row for the rest: its note says it") | |
| 202 | + | _, _, page = h.get("/wide/tree/main") | |
| 203 | + | assert.NotContains(t, page, `class="note"`, "a directory that fits has no note") | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | // Entries that are not plain files or directories say what they are in words | |
| 207 | + | // (the node drawn for them is aria-hidden), and a name is text: HTML in it is | |
| 208 | + | // escaped and invisible characters show as markers. | |
| 209 | + | func TestWebTreeEntryKinds(t *testing.T) { | |
| 210 | + | h := newHarness(t, harnessOpts{}) | |
| 211 | + | _, err := h.forge.Repos.Create(h.ctx, domain.Root(), ports.CreateRepoParams{Name: h.name("kinds"), Visibility: domain.VisibilityPublic}) | |
| 212 | + | require.NoError(t, err) | |
| 213 | + | wt := h.t.TempDir() | |
| 214 | + | h.mustGit(wt, "init", "-q", "--initial-branch=main") | |
| 215 | + | h.writeFile(wt, "target.txt", []byte("t\n")) | |
| 216 | + | h.writeFile(wt, `odd<b>&"name.txt`, []byte("x\n")) | |
| 217 | + | h.writeFile(wt, "bidi\u202eevil.txt", []byte("x\n")) | |
| 218 | + | h.writeFile(wt, "dir/inner.txt", []byte("x\n")) | |
| 219 | + | // A symbolic link and a submodule entry (a gitlink: no clone is needed). | |
| 220 | + | require.NoError(t, os.Symlink("target.txt", filepath.Join(wt, "link.txt"))) | |
| 221 | + | h.mustGit(wt, "add", "link.txt") | |
| 222 | + | h.mustGit(wt, "update-index", "--add", "--cacheinfo", "160000,"+strings.TrimSpace(h.mustGit(wt, "rev-parse", "HEAD"))+",vendor") | |
| 223 | + | h.mustGit(wt, "commit", "-q", "-m", "kinds") | |
| 224 | + | h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/kinds.git"), "main") | |
| 225 | + | ||
| 226 | + | status, _, page := h.get("/kinds/tree/main") | |
| 227 | + | require.Equal(t, 200, status) | |
| 228 | + | assertNoInline(t, page) | |
| 229 | + | ||
| 230 | + | // A symbolic link has a link to its page, and says what it is. | |
| 231 | + | assert.Contains(t, page, `data-kind="link"`) | |
| 232 | + | assert.Contains(t, page, `<a class="t-name" href="/kinds/blob/main/link.txt" title="symbolic link">link.txt</a> <span class="sr"> (symbolic link)</span>`) | |
| 233 | + | // A submodule has no page here: a name that is not a link. | |
| 234 | + | assert.Contains(t, page, `data-kind="module"`) | |
| 235 | + | assert.Contains(t, page, `<span class="t-name" title="submodule">vendor</span> <span class="sr"> (submodule)</span>`) | |
| 236 | + | // A directory says so too, and a file says nothing more. | |
| 237 | + | assert.Contains(t, page, `title="directory">dir/</a> <span class="sr"> (directory)</span>`) | |
| 238 | + | assert.Contains(t, page, `href="/kinds/blob/main/target.txt">target.txt</a> </span>`) | |
| 239 | + | ||
| 240 | + | // HTML in a name is escaped; an invisible character shows as its marker. | |
| 241 | + | assert.NotContains(t, page, "<b>&") | |
| 242 | + | assert.Contains(t, page, `odd<b>&"name.txt`) | |
| 243 | + | assert.Contains(t, page, "bidi<U+202E>evil.txt") | |
| 244 | + | assert.NotContains(t, page, "\u202e") | |
| 179 | 245 | } | |
| 180 | 246 | ||
| 181 | 247 | // Ref names with a slash are found by their longest prefix, and a commit id | |
| @@ -201,10 +267,13 @@ func TestWebFilesRefs(t *testing.T) { | |||
| 201 | 267 | status, _, _ = h.get("/files/tree/" + sha[:7]) | |
| 202 | 268 | assert.Equal(t, 200, status, "an abbreviated commit id") | |
| 203 | 269 | ||
| 204 | - | // Links of a directory README stay under the directory and the ref. | |
| 270 | + | // The entries of a directory stay under the directory and the ref, and the | |
| 271 | + | // tree goes up to the parent. | |
| 205 | 272 | _, _, body = h.get("/files/tree/main/docs") | |
| 206 | 273 | assert.Contains(t, body, `href="/files/blob/main/docs/guide.md"`) | |
| 207 | - | assert.Contains(t, body, `href="/files/blob/main/main.go"`) | |
| 274 | + | assert.Contains(t, body, `data-up="/files/tree/main"`) | |
| 275 | + | _, _, body = h.get("/files/tree/main/src/lib") | |
| 276 | + | assert.Contains(t, body, `data-up="/files/tree/main/src"`) | |
| 208 | 277 | } | |
| 209 | 278 | ||
| 210 | 279 | // AC3: what does not exist is the 404 page, with no hint of why. | |
| @@ -574,7 +643,7 @@ func TestWebFinderPage(t *testing.T) { | |||
| 574 | 643 | if strings.Contains(tt.path, "/tree/") { | |
| 575 | 644 | assert.Contains(t, body, `id="finder-q"`) | |
| 576 | 645 | assert.Contains(t, body, `id="finder-list"`) | |
| 577 | - | assert.Contains(t, body, `placeholder="Find a file…"`) | |
| 646 | + | assert.Contains(t, body, `placeholder="Find a file in this repository…"`) | |
| 578 | 647 | } else { | |
| 579 | 648 | assert.NotContains(t, body, `id="finder-q"`) | |
| 580 | 649 | } | |
| @@ -116,7 +116,15 @@ func TestWebTreeRows(t *testing.T) { | |||||
| 116 | 116 | ||||
| 117 | // The page links its directories to their rows. | 117 | // The page links its directories to their rows. | ||
| 118 | _, _, page := h.get("/files/tree/main") | 118 | _, _, page := h.get("/files/tree/main") | ||
| 119 | - | assert.Contains(t, page, `href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false"`) | 119 | + | assert.Contains(t, page, `data-expand="/files/tree/main/docs?rows" aria-expanded="false"`) |
| 120 | + | assert.Contains(t, page, `<a class="t-name" href="/files/tree/main/docs" title="directory">docs/</a>`) | |||
| 121 | + | // Every row says its place among the entries of its own directory. | |||
| 122 | + | assert.Contains(t, page, `aria-level="1" aria-posinset="1" aria-setsize="15" data-kind="dir"`) | |||
| 123 | + | assert.Contains(t, page, `aria-posinset="15" aria-setsize="15" data-kind="file"`) | |||
| 124 | + | assert.Equal(t, 15, strings.Count(page, `role="treeitem"`)) | |||
| 125 | + | assert.Contains(t, page, `<a class="t-commit" href="/files/commit/`) | |||
| 126 | + | assert.NotContains(t, page, "<h1", "no README below the tree: the summary shows it") | |||
| 127 | + | assert.NotContains(t, page, "See the guide") | |||
| 120 | assert.Contains(t, page, `<script src="/-/static/js/files.`) | 128 | assert.Contains(t, page, `<script src="/-/static/js/files.`) | ||
| 121 | assert.NotContains(t, page, "main.go</span>", "the palette lists no file") | 129 | assert.NotContains(t, page, "main.go</span>", "the palette lists no file") | ||
| 122 | 130 | ||||
| @@ -126,16 +134,19 @@ func TestWebTreeRows(t *testing.T) { | |||||
| 126 | assert.Contains(t, hdr.Get("Content-Type"), "text/html") | 134 | assert.Contains(t, hdr.Get("Content-Type"), "text/html") | ||
| 127 | assert.NotContains(t, body, "<html") | 135 | assert.NotContains(t, body, "<html") | ||
| 128 | assert.NotContains(t, body, "<table") | 136 | assert.NotContains(t, body, "<table") | ||
| 129 | - | assert.Equal(t, 2, strings.Count(body, "<tr"), "one row per entry") | 137 | + | assert.NotContains(t, body, "<ul") |
| 130 | - | assert.Contains(t, body, `<a href="/files/blob/main/docs/README.md">README.md</a>`) | 138 | + | assert.Equal(t, 2, strings.Count(body, "<li"), "one row per entry") |
| 131 | - | assert.Contains(t, body, `<a href="/files/blob/main/docs/guide.md">guide.md</a>`) | 139 | + | assert.Contains(t, body, `aria-posinset="1" aria-setsize="2"`) |
| 140 | + | assert.Contains(t, body, `aria-posinset="2" aria-setsize="2"`, "positions count the entries of the directory itself") | |||
| 141 | + | assert.Contains(t, body, `<a class="t-name" href="/files/blob/main/docs/README.md">README.md</a>`) | |||
| 142 | + | assert.Contains(t, body, `<a class="t-name" href="/files/blob/main/docs/guide.md">guide.md</a>`) | |||
| 132 | assert.NotContains(t, body, "data-cursor", "the cursor stays on the page") | 143 | assert.NotContains(t, body, "data-cursor", "the cursor stays on the page") | ||
| 133 | assertNoInline(t, body) | 144 | assertNoInline(t, body) | ||
| 134 | 145 | ||||
| 135 | // A subdirectory row can be opened in turn. | 146 | // A subdirectory row can be opened in turn. | ||
| 136 | _, _, body = h.get("/files/tree/main/src?rows") | 147 | _, _, body = h.get("/files/tree/main/src?rows") | ||
| 137 | - | assert.Contains(t, body, `href="/files/tree/main/src/lib" data-expand="/files/tree/main/src/lib?rows" aria-expanded="false"`) | 148 | + | assert.Contains(t, body, `data-expand="/files/tree/main/src/lib?rows" aria-expanded="false"`) |
| 138 | - | assert.Contains(t, body, "▸") | 149 | + | assert.Contains(t, body, `data-kind="dir"`) |
| 139 | _, _, body = h.get("/files/tree/main/src/lib?rows") | 150 | _, _, body = h.get("/files/tree/main/src/lib?rows") | ||
| 140 | assert.Contains(t, body, `href="/files/blob/main/src/lib/util.go"`) | 151 | assert.Contains(t, body, `href="/files/blob/main/src/lib/util.go"`) | ||
| 141 | assert.NotContains(t, body, "data-expand") | 152 | assert.NotContains(t, body, "data-expand") | ||
| @@ -169,13 +180,68 @@ func TestWebTreeRowsTruncated(t *testing.T) { | |||||
| 169 | 180 | ||||
| 170 | status, _, body := h.get("/wide/tree/main/many?rows") | 181 | status, _, body := h.get("/wide/tree/main/many?rows") | ||
| 171 | require.Equal(t, 200, status) | 182 | require.Equal(t, 200, status) | ||
| 172 | - | assert.Equal(t, domain.MaxTreeEntries+1, strings.Count(body, "<tr"), "the listed entries and the row that says there are more") | 183 | + | assert.Equal(t, domain.MaxTreeEntries+1, strings.Count(body, "<li"), "the listed entries and the row that says there are more") |
| 173 | - | assert.Contains(t, body, `<tr class="tree-more"><td colspan="7"><a href="/wide/tree/main/many">more entries: open the directory</a></td></tr>`) | 184 | + | assert.Contains(t, body, `<a class="t-name" href="/wide/tree/main/many">more entries: open the directory</a>`) |
| 174 | assert.Equal(t, 1, strings.Count(body, "tree-more")) | 185 | assert.Equal(t, 1, strings.Count(body, "tree-more")) | ||
| 186 | + | // The listing is cut: the size is unknown (-1), and the row that says so is the next position. | |||
| 187 | + | assert.Equal(t, domain.MaxTreeEntries, strings.Count(body, `aria-setsize="-1" data-kind=`)) | |||
| 188 | + | assert.Contains(t, body, `aria-posinset="1" aria-setsize="-1" data-kind="file"`) | |||
| 189 | + | assert.Contains(t, body, fmt.Sprintf(`class="tree-more" role="treeitem" aria-level="1" aria-posinset="%d" aria-setsize="-1"`, domain.MaxTreeEntries+1)) | |||
| 175 | 190 | ||||
| 176 | // A directory that fits has no such row. | 191 | // A directory that fits has no such row. | ||
| 177 | _, _, body = h.get("/wide/tree?rows") | 192 | _, _, body = h.get("/wide/tree?rows") | ||
| 178 | assert.NotContains(t, body, "tree-more") | 193 | assert.NotContains(t, body, "tree-more") | ||
| 194 | + | ||||
| 195 | + | // The page of the directory says the listing is cut, in a note, and its | |||
| 196 | + | // rows are of unknown number. | |||
| 197 | + | status, _, page := h.get("/wide/tree/main/many") | |||
| 198 | + | require.Equal(t, 200, status) | |||
| 199 | + | assert.Contains(t, page, fmt.Sprintf(`<p class="note">this directory has more entries than are listed here: the first %d are shown.</p>`, domain.MaxTreeEntries)) | |||
| 200 | + | assert.Contains(t, page, `aria-posinset="1" aria-setsize="-1" data-kind="file"`) | |||
| 201 | + | assert.NotContains(t, page, "tree-more", "the page of the directory has no row for the rest: its note says it") | |||
| 202 | + | _, _, page = h.get("/wide/tree/main") | |||
| 203 | + | assert.NotContains(t, page, `class="note"`, "a directory that fits has no note") | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | // Entries that are not plain files or directories say what they are in words | |||
| 207 | + | // (the node drawn for them is aria-hidden), and a name is text: HTML in it is | |||
| 208 | + | // escaped and invisible characters show as markers. | |||
| 209 | + | func TestWebTreeEntryKinds(t *testing.T) { | |||
| 210 | + | h := newHarness(t, harnessOpts{}) | |||
| 211 | + | _, err := h.forge.Repos.Create(h.ctx, domain.Root(), ports.CreateRepoParams{Name: h.name("kinds"), Visibility: domain.VisibilityPublic}) | |||
| 212 | + | require.NoError(t, err) | |||
| 213 | + | wt := h.t.TempDir() | |||
| 214 | + | h.mustGit(wt, "init", "-q", "--initial-branch=main") | |||
| 215 | + | h.writeFile(wt, "target.txt", []byte("t\n")) | |||
| 216 | + | h.writeFile(wt, `odd<b>&"name.txt`, []byte("x\n")) | |||
| 217 | + | h.writeFile(wt, "bidi\u202eevil.txt", []byte("x\n")) | |||
| 218 | + | h.writeFile(wt, "dir/inner.txt", []byte("x\n")) | |||
| 219 | + | // A symbolic link and a submodule entry (a gitlink: no clone is needed). | |||
| 220 | + | require.NoError(t, os.Symlink("target.txt", filepath.Join(wt, "link.txt"))) | |||
| 221 | + | h.mustGit(wt, "add", "link.txt") | |||
| 222 | + | h.mustGit(wt, "update-index", "--add", "--cacheinfo", "160000,"+strings.TrimSpace(h.mustGit(wt, "rev-parse", "HEAD"))+",vendor") | |||
| 223 | + | h.mustGit(wt, "commit", "-q", "-m", "kinds") | |||
| 224 | + | h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/kinds.git"), "main") | |||
| 225 | + | ||||
| 226 | + | status, _, page := h.get("/kinds/tree/main") | |||
| 227 | + | require.Equal(t, 200, status) | |||
| 228 | + | assertNoInline(t, page) | |||
| 229 | + | ||||
| 230 | + | // A symbolic link has a link to its page, and says what it is. | |||
| 231 | + | assert.Contains(t, page, `data-kind="link"`) | |||
| 232 | + | assert.Contains(t, page, `<a class="t-name" href="/kinds/blob/main/link.txt" title="symbolic link">link.txt</a> <span class="sr"> (symbolic link)</span>`) | |||
| 233 | + | // A submodule has no page here: a name that is not a link. | |||
| 234 | + | assert.Contains(t, page, `data-kind="module"`) | |||
| 235 | + | assert.Contains(t, page, `<span class="t-name" title="submodule">vendor</span> <span class="sr"> (submodule)</span>`) | |||
| 236 | + | // A directory says so too, and a file says nothing more. | |||
| 237 | + | assert.Contains(t, page, `title="directory">dir/</a> <span class="sr"> (directory)</span>`) | |||
| 238 | + | assert.Contains(t, page, `href="/kinds/blob/main/target.txt">target.txt</a> </span>`) | |||
| 239 | + | ||||
| 240 | + | // HTML in a name is escaped; an invisible character shows as its marker. | |||
| 241 | + | assert.NotContains(t, page, "<b>&") | |||
| 242 | + | assert.Contains(t, page, `odd<b>&"name.txt`) | |||
| 243 | + | assert.Contains(t, page, "bidi<U+202E>evil.txt") | |||
| 244 | + | assert.NotContains(t, page, "\u202e") | |||
| 179 | } | 245 | } | ||
| 180 | 246 | ||||
| 181 | // Ref names with a slash are found by their longest prefix, and a commit id | 247 | // Ref names with a slash are found by their longest prefix, and a commit id | ||
| @@ -201,10 +267,13 @@ func TestWebFilesRefs(t *testing.T) { | |||||
| 201 | status, _, _ = h.get("/files/tree/" + sha[:7]) | 267 | status, _, _ = h.get("/files/tree/" + sha[:7]) | ||
| 202 | assert.Equal(t, 200, status, "an abbreviated commit id") | 268 | assert.Equal(t, 200, status, "an abbreviated commit id") | ||
| 203 | 269 | ||||
| 204 | - | // Links of a directory README stay under the directory and the ref. | 270 | + | // The entries of a directory stay under the directory and the ref, and the |
| 271 | + | // tree goes up to the parent. | |||
| 205 | _, _, body = h.get("/files/tree/main/docs") | 272 | _, _, body = h.get("/files/tree/main/docs") | ||
| 206 | assert.Contains(t, body, `href="/files/blob/main/docs/guide.md"`) | 273 | assert.Contains(t, body, `href="/files/blob/main/docs/guide.md"`) | ||
| 207 | - | assert.Contains(t, body, `href="/files/blob/main/main.go"`) | 274 | + | assert.Contains(t, body, `data-up="/files/tree/main"`) |
| 275 | + | _, _, body = h.get("/files/tree/main/src/lib") | |||
| 276 | + | assert.Contains(t, body, `data-up="/files/tree/main/src"`) | |||
| 208 | } | 277 | } | ||
| 209 | 278 | ||||
| 210 | // AC3: what does not exist is the 404 page, with no hint of why. | 279 | // AC3: what does not exist is the 404 page, with no hint of why. | ||
| @@ -574,7 +643,7 @@ func TestWebFinderPage(t *testing.T) { | |||||
| 574 | if strings.Contains(tt.path, "/tree/") { | 643 | if strings.Contains(tt.path, "/tree/") { | ||
| 575 | assert.Contains(t, body, `id="finder-q"`) | 644 | assert.Contains(t, body, `id="finder-q"`) | ||
| 576 | assert.Contains(t, body, `id="finder-list"`) | 645 | assert.Contains(t, body, `id="finder-list"`) | ||
| 577 | - | assert.Contains(t, body, `placeholder="Find a file…"`) | 646 | + | assert.Contains(t, body, `placeholder="Find a file in this repository…"`) |
| 578 | } else { | 647 | } else { | ||
| 579 | assert.NotContains(t, body, `id="finder-q"`) | 648 | assert.NotContains(t, body, `id="finder-q"`) | ||
| 580 | } | 649 | } | ||