fix(web): MR description as Markdown, latest tag row inline, no install button (#60)
- commit
- c1179903253ff4075db07b94eb163628bc5d2851
- author
- brice · · 2026-10-18 09:06 UTC
- committer
- bnema · · 2026-10-18 09:06 UTC
- parent
- ef38ad0
- signature
- SSH Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe key is unknown
46 files · +491 -695
M internal/adapters/web/ASSETS.md +1 -1
| @@ -16,7 +16,7 @@ content-hashed names. Nothing is fetched at runtime. | |||
| 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 | | | |
| 17 | 17 | | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` | | |
| 18 | 18 | | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` | | |
| 19 | - | | `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is. Only the icons in use are copied, one file each: `branch`, `search`, `tag`, `file`, `chevron`, `user`, `key`, `log-in`, `log-out`, `download` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`, `download`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | | |
| 19 | + | | `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is. Only the icons in use are copied, one file each: `branch`, `search`, `tag`, `file`, `chevron`, `user`, `key`, `log-in`, `log-out` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | | |
| 20 | 20 | | `static/app/*.png` | icons of the installed web app (manifest, `apple-touch-icon`), rendered from `brand.svg` with resvg; the maskable and Apple icons have the picture in the safe zone on `#181818` | generated here | | | |
| 21 | 21 | | `static/sw.js` | the service worker of the installed app: a network-only passthrough (no cache), served at `/-/sw.js` with `Service-Worker-Allowed: /` and revalidated on each check (not a hashed asset) | written here | | | |
| 22 | 22 | | `static/rules.json` | speculation rules: prefetch the light same-origin pages on hover (`Speculation-Rules` header); the heavy ones (tree, blob, commit, commits, compare, refs, raw, MR "since") are excluded, each costs several Git reads on the server | written here | | | |
| @@ -16,7 +16,7 @@ content-hashed names. Nothing is fetched at runtime. | |||||
| 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 | | | ||
| 17 | | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` | | 17 | | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` | | ||
| 18 | | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` | | 18 | | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` | | ||
| 19 | - | | `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is. Only the icons in use are copied, one file each: `branch`, `search`, `tag`, `file`, `chevron`, `user`, `key`, `log-in`, `log-out`, `download` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`, `download`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | | 19 | + | | `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is. Only the icons in use are copied, one file each: `branch`, `search`, `tag`, `file`, `chevron`, `user`, `key`, `log-in`, `log-out` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | |
| 20 | | `static/app/*.png` | icons of the installed web app (manifest, `apple-touch-icon`), rendered from `brand.svg` with resvg; the maskable and Apple icons have the picture in the safe zone on `#181818` | generated here | | | 20 | | `static/app/*.png` | icons of the installed web app (manifest, `apple-touch-icon`), rendered from `brand.svg` with resvg; the maskable and Apple icons have the picture in the safe zone on `#181818` | generated here | | | ||
| 21 | | `static/sw.js` | the service worker of the installed app: a network-only passthrough (no cache), served at `/-/sw.js` with `Service-Worker-Allowed: /` and revalidated on each check (not a hashed asset) | written here | | | 21 | | `static/sw.js` | the service worker of the installed app: a network-only passthrough (no cache), served at `/-/sw.js` with `Service-Worker-Allowed: /` and revalidated on each check (not a hashed asset) | written here | | | ||
| 22 | | `static/rules.json` | speculation rules: prefetch the light same-origin pages on hover (`Speculation-Rules` header); the heavy ones (tree, blob, commit, commits, compare, refs, raw, MR "since") are excluded, each costs several Git reads on the server | written here | | | 22 | | `static/rules.json` | speculation rules: prefetch the light same-origin pages on hover (`Speculation-Rules` header); the heavy ones (tree, blob, commit, commits, compare, refs, raw, MR "since") are excluded, each costs several Git reads on the server | written here | | | ||
M internal/adapters/web/jstest/install.test.mjs +5 -71
| @@ -1,22 +1,15 @@ | |||
| 1 | 1 | // Tests of the install block of static/js/ermite.js (ermiteInstall): the | |
| 2 | - | // service worker registration and the "install app" button. Run with | |
| 2 | + | // service worker registration. Run with | |
| 3 | 3 | // node --test 'internal/adapters/web/jstest/*.test.mjs' | |
| 4 | 4 | import assert from 'node:assert/strict'; | |
| 5 | 5 | import { readFileSync } from 'node:fs'; | |
| 6 | 6 | import { test } from 'node:test'; | |
| 7 | - | import { dom, el, installDom } from './dom.mjs'; | |
| 7 | + | import { installDom } from './dom.mjs'; | |
| 8 | 8 | ||
| 9 | 9 | const doc = installDom(); | |
| 10 | - | const button = el('button', { attrs: { 'data-install': '' } }); | |
| 11 | - | button.hidden = true; | |
| 12 | - | document.body.append(button); | |
| 13 | - | ||
| 14 | 10 | const setNavigator = (v) => Object.defineProperty(globalThis, 'navigator', { value: v, configurable: true, writable: true }); | |
| 15 | 11 | const listeners = {}; | |
| 16 | 12 | globalThis.addEventListener = (type, fn) => { listeners[type] = fn; }; | |
| 17 | - | const fire = (type, ev) => listeners[type](ev); | |
| 18 | - | let standalone = false; | |
| 19 | - | globalThis.matchMedia = () => ({ matches: standalone }); | |
| 20 | 13 | doc.readyState = 'loading'; | |
| 21 | 14 | const registered = []; | |
| 22 | 15 | setNavigator({ serviceWorker: { register: async (url, opts) => { registered.push([url, opts]); return {}; } } }); | |
| @@ -25,17 +18,9 @@ setNavigator({ serviceWorker: { register: async (url, opts) => { registered.push | |||
| 25 | 18 | const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8'); | |
| 26 | 19 | const start = src.indexOf(' window.ermiteInstall = '); | |
| 27 | 20 | const end = src.indexOf(' })();', start) + ' })();'.length; | |
| 28 | - | new Function('$$', src.slice(start, end))((sel) => document.querySelectorAll(sel)); | |
| 21 | + | new Function(src.slice(start, end))(); | |
| 29 | 22 | const I = window.ermiteInstall; | |
| 30 | 23 | ||
| 31 | - | // promptEvent is a beforeinstallprompt event. | |
| 32 | - | function promptEvent(outcome = 'accepted') { | |
| 33 | - | const e = { prevented: false, prompts: 0, preventDefault() { this.prevented = true; } }; | |
| 34 | - | e.prompt = async () => { e.prompts++; }; | |
| 35 | - | e.userChoice = Promise.resolve({ outcome }); | |
| 36 | - | return e; | |
| 37 | - | } | |
| 38 | - | ||
| 39 | 24 | test('the service worker is registered at the root scope once the page has loaded', async () => { | |
| 40 | 25 | assert.equal(registered.length, 0, 'not before load'); | |
| 41 | 26 | await listeners.load(); | |
| @@ -49,57 +34,6 @@ test('a refused registration is not an error', async () => { | |||
| 49 | 34 | assert.equal(await I.register(), null); | |
| 50 | 35 | }); | |
| 51 | 36 | ||
| 52 | - | test('the button shows on beforeinstallprompt, and a click prompts once', async () => { | |
| 53 | - | const e = promptEvent(); | |
| 54 | - | assert.equal(button.hidden, true); | |
| 55 | - | fire('beforeinstallprompt', e); | |
| 56 | - | assert.equal(e.prevented, true, 'the browser banner is replaced by the button'); | |
| 57 | - | assert.equal(button.hidden, false); | |
| 58 | - | const done = listeners.click({ target: { closest: (s) => (s === '[data-install]' ? button : null) } }); | |
| 59 | - | assert.equal(done, undefined); | |
| 60 | - | assert.equal(await I.pending, 'accepted'); | |
| 61 | - | assert.equal(await I.prompt(), '', 'the event was used by the click'); | |
| 62 | - | assert.equal(e.prompts, 1); | |
| 63 | - | assert.equal(button.hidden, true); | |
| 64 | - | }); | |
| 65 | - | ||
| 66 | - | test('the button stays while the dialog is up; its focus then moves to the search', async () => { | |
| 67 | - | const search = el('button', { attrs: { 'aria-controls': 'panel-cmd' } }); | |
| 68 | - | document.body.append(search); | |
| 69 | - | const e = promptEvent('dismissed'); | |
| 70 | - | let shownDuring = null; | |
| 71 | - | e.prompt = async () => { shownDuring = !button.hidden; }; | |
| 72 | - | fire('beforeinstallprompt', e); | |
| 73 | - | button.focus(); | |
| 74 | - | dom.focused = []; | |
| 75 | - | assert.equal(await I.prompt(), 'dismissed'); | |
| 76 | - | assert.equal(shownDuring, true, 'not hidden under the dialog'); | |
| 77 | - | assert.equal(button.hidden, true); | |
| 78 | - | assert.deepEqual(dom.focused, [search]); | |
| 79 | - | dom.active = null; | |
| 80 | - | search.remove(); | |
| 81 | - | }); | |
| 82 | - | ||
| 83 | - | test('prompt without an event does nothing; a dismissal reports its outcome', async () => { | |
| 84 | - | assert.equal(await I.prompt(), ''); | |
| 85 | - | const e = promptEvent('dismissed'); | |
| 86 | - | fire('beforeinstallprompt', e); | |
| 87 | - | assert.equal(await I.prompt(), 'dismissed'); | |
| 88 | - | assert.equal(button.hidden, true); | |
| 89 | - | }); | |
| 90 | - | ||
| 91 | - | test('appinstalled hides the button', () => { | |
| 92 | - | fire('beforeinstallprompt', promptEvent()); | |
| 93 | - | assert.equal(button.hidden, false); | |
| 94 | - | fire('appinstalled', {}); | |
| 95 | - | assert.equal(button.hidden, true); | |
| 96 | - | }); | |
| 97 | - | ||
| 98 | - | test('an app that runs installed never shows the button', () => { | |
| 99 | - | standalone = true; | |
| 100 | - | const e = promptEvent(); | |
| 101 | - | fire('beforeinstallprompt', e); | |
| 102 | - | assert.equal(button.hidden, true); | |
| 103 | - | assert.equal(e.prevented, false); | |
| 104 | - | standalone = false; | |
| 37 | + | test('the page leaves the browser its own install offer', () => { | |
| 38 | + | assert.equal(listeners.beforeinstallprompt, undefined, 'no preventDefault on beforeinstallprompt'); | |
| 105 | 39 | }); | |
| @@ -1,22 +1,15 @@ | |||||
| 1 | // Tests of the install block of static/js/ermite.js (ermiteInstall): the | 1 | // Tests of the install block of static/js/ermite.js (ermiteInstall): the | ||
| 2 | - | // service worker registration and the "install app" button. Run with | 2 | + | // service worker registration. Run with |
| 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 { readFileSync } from 'node:fs'; | 5 | import { readFileSync } from 'node:fs'; | ||
| 6 | import { test } from 'node:test'; | 6 | import { test } from 'node:test'; | ||
| 7 | - | import { dom, el, installDom } from './dom.mjs'; | 7 | + | import { installDom } from './dom.mjs'; |
| 8 | 8 | ||||
| 9 | const doc = installDom(); | 9 | const doc = installDom(); | ||
| 10 | - | const button = el('button', { attrs: { 'data-install': '' } }); | |||
| 11 | - | button.hidden = true; | |||
| 12 | - | document.body.append(button); | |||
| 13 | - | ||||
| 14 | const setNavigator = (v) => Object.defineProperty(globalThis, 'navigator', { value: v, configurable: true, writable: true }); | 10 | const setNavigator = (v) => Object.defineProperty(globalThis, 'navigator', { value: v, configurable: true, writable: true }); | ||
| 15 | const listeners = {}; | 11 | const listeners = {}; | ||
| 16 | globalThis.addEventListener = (type, fn) => { listeners[type] = fn; }; | 12 | globalThis.addEventListener = (type, fn) => { listeners[type] = fn; }; | ||
| 17 | - | const fire = (type, ev) => listeners[type](ev); | |||
| 18 | - | let standalone = false; | |||
| 19 | - | globalThis.matchMedia = () => ({ matches: standalone }); | |||
| 20 | doc.readyState = 'loading'; | 13 | doc.readyState = 'loading'; | ||
| 21 | const registered = []; | 14 | const registered = []; | ||
| 22 | setNavigator({ serviceWorker: { register: async (url, opts) => { registered.push([url, opts]); return {}; } } }); | 15 | setNavigator({ serviceWorker: { register: async (url, opts) => { registered.push([url, opts]); return {}; } } }); | ||
| @@ -25,17 +18,9 @@ setNavigator({ serviceWorker: { register: async (url, opts) => { registered.push | |||||
| 25 | const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8'); | 18 | const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8'); | ||
| 26 | const start = src.indexOf(' window.ermiteInstall = '); | 19 | const start = src.indexOf(' window.ermiteInstall = '); | ||
| 27 | const end = src.indexOf(' })();', start) + ' })();'.length; | 20 | const end = src.indexOf(' })();', start) + ' })();'.length; | ||
| 28 | - | new Function('$$', src.slice(start, end))((sel) => document.querySelectorAll(sel)); | 21 | + | new Function(src.slice(start, end))(); |
| 29 | const I = window.ermiteInstall; | 22 | const I = window.ermiteInstall; | ||
| 30 | 23 | ||||
| 31 | - | // promptEvent is a beforeinstallprompt event. | |||
| 32 | - | function promptEvent(outcome = 'accepted') { | |||
| 33 | - | const e = { prevented: false, prompts: 0, preventDefault() { this.prevented = true; } }; | |||
| 34 | - | e.prompt = async () => { e.prompts++; }; | |||
| 35 | - | e.userChoice = Promise.resolve({ outcome }); | |||
| 36 | - | return e; | |||
| 37 | - | } | |||
| 38 | - | ||||
| 39 | test('the service worker is registered at the root scope once the page has loaded', async () => { | 24 | test('the service worker is registered at the root scope once the page has loaded', async () => { | ||
| 40 | assert.equal(registered.length, 0, 'not before load'); | 25 | assert.equal(registered.length, 0, 'not before load'); | ||
| 41 | await listeners.load(); | 26 | await listeners.load(); | ||
| @@ -49,57 +34,6 @@ test('a refused registration is not an error', async () => { | |||||
| 49 | assert.equal(await I.register(), null); | 34 | assert.equal(await I.register(), null); | ||
| 50 | }); | 35 | }); | ||
| 51 | 36 | ||||
| 52 | - | test('the button shows on beforeinstallprompt, and a click prompts once', async () => { | 37 | + | test('the page leaves the browser its own install offer', () => { |
| 53 | - | const e = promptEvent(); | 38 | + | assert.equal(listeners.beforeinstallprompt, undefined, 'no preventDefault on beforeinstallprompt'); |
| 54 | - | assert.equal(button.hidden, true); | |||
| 55 | - | fire('beforeinstallprompt', e); | |||
| 56 | - | assert.equal(e.prevented, true, 'the browser banner is replaced by the button'); | |||
| 57 | - | assert.equal(button.hidden, false); | |||
| 58 | - | const done = listeners.click({ target: { closest: (s) => (s === '[data-install]' ? button : null) } }); | |||
| 59 | - | assert.equal(done, undefined); | |||
| 60 | - | assert.equal(await I.pending, 'accepted'); | |||
| 61 | - | assert.equal(await I.prompt(), '', 'the event was used by the click'); | |||
| 62 | - | assert.equal(e.prompts, 1); | |||
| 63 | - | assert.equal(button.hidden, true); | |||
| 64 | - | }); | |||
| 65 | - | ||||
| 66 | - | test('the button stays while the dialog is up; its focus then moves to the search', async () => { | |||
| 67 | - | const search = el('button', { attrs: { 'aria-controls': 'panel-cmd' } }); | |||
| 68 | - | document.body.append(search); | |||
| 69 | - | const e = promptEvent('dismissed'); | |||
| 70 | - | let shownDuring = null; | |||
| 71 | - | e.prompt = async () => { shownDuring = !button.hidden; }; | |||
| 72 | - | fire('beforeinstallprompt', e); | |||
| 73 | - | button.focus(); | |||
| 74 | - | dom.focused = []; | |||
| 75 | - | assert.equal(await I.prompt(), 'dismissed'); | |||
| 76 | - | assert.equal(shownDuring, true, 'not hidden under the dialog'); | |||
| 77 | - | assert.equal(button.hidden, true); | |||
| 78 | - | assert.deepEqual(dom.focused, [search]); | |||
| 79 | - | dom.active = null; | |||
| 80 | - | search.remove(); | |||
| 81 | - | }); | |||
| 82 | - | ||||
| 83 | - | test('prompt without an event does nothing; a dismissal reports its outcome', async () => { | |||
| 84 | - | assert.equal(await I.prompt(), ''); | |||
| 85 | - | const e = promptEvent('dismissed'); | |||
| 86 | - | fire('beforeinstallprompt', e); | |||
| 87 | - | assert.equal(await I.prompt(), 'dismissed'); | |||
| 88 | - | assert.equal(button.hidden, true); | |||
| 89 | - | }); | |||
| 90 | - | ||||
| 91 | - | test('appinstalled hides the button', () => { | |||
| 92 | - | fire('beforeinstallprompt', promptEvent()); | |||
| 93 | - | assert.equal(button.hidden, false); | |||
| 94 | - | fire('appinstalled', {}); | |||
| 95 | - | assert.equal(button.hidden, true); | |||
| 96 | - | }); | |||
| 97 | - | ||||
| 98 | - | test('an app that runs installed never shows the button', () => { | |||
| 99 | - | standalone = true; | |||
| 100 | - | const e = promptEvent(); | |||
| 101 | - | fire('beforeinstallprompt', e); | |||
| 102 | - | assert.equal(button.hidden, true); | |||
| 103 | - | assert.equal(e.prevented, false); | |||
| 104 | - | standalone = false; | |||
| 105 | }); | 39 | }); | ||
M internal/adapters/web/manifest_test.go +0 -1
| @@ -57,7 +57,6 @@ func TestWebAppManifest(t *testing.T) { | |||
| 57 | 57 | page := renderToString(t, Shell(Layout{Assets: u}, nil)) | |
| 58 | 58 | assert.Contains(t, page, `<link rel="manifest" href="`+u.Manifest+`">`) | |
| 59 | 59 | assert.Contains(t, page, `<link rel="apple-touch-icon" href="`+u.AppleIcon+`">`) | |
| 60 | - | assert.Contains(t, page, `data-install`) | |
| 61 | 60 | } | |
| 62 | 61 | ||
| 63 | 62 | // The icons are exactly the size the manifest declares: Chrome does not | |
| @@ -57,7 +57,6 @@ func TestWebAppManifest(t *testing.T) { | |||||
| 57 | page := renderToString(t, Shell(Layout{Assets: u}, nil)) | 57 | page := renderToString(t, Shell(Layout{Assets: u}, nil)) | ||
| 58 | assert.Contains(t, page, `<link rel="manifest" href="`+u.Manifest+`">`) | 58 | assert.Contains(t, page, `<link rel="manifest" href="`+u.Manifest+`">`) | ||
| 59 | assert.Contains(t, page, `<link rel="apple-touch-icon" href="`+u.AppleIcon+`">`) | 59 | assert.Contains(t, page, `<link rel="apple-touch-icon" href="`+u.AppleIcon+`">`) | ||
| 60 | - | assert.Contains(t, page, `data-install`) | |||
| 61 | } | 60 | } | ||
| 62 | 61 | ||||
| 63 | // The icons are exactly the size the manifest declares: Chrome does not | 62 | // The icons are exactly the size the manifest declares: Chrome does not | ||
M internal/adapters/web/mr.go +14 -2
| @@ -5,6 +5,7 @@ package web | |||
| 5 | 5 | import ( | |
| 6 | 6 | "context" | |
| 7 | 7 | "html" | |
| 8 | + | "html/template" | |
| 8 | 9 | "io" | |
| 9 | 10 | "net/http" | |
| 10 | 11 | "net/url" | |
| @@ -196,7 +197,9 @@ type MRPage struct { | |||
| 196 | 197 | Opened Person | |
| 197 | 198 | Updated string | |
| 198 | 199 | UpdatedAt string | |
| 199 | - | Desc []TextPart | |
| 200 | + | // Desc is the description rendered as Markdown (as a README: no raw | |
| 201 | + | // HTML, filtered URLs), "" when it is empty. | |
| 202 | + | Desc template.HTML | |
| 200 | 203 | Head string | |
| 201 | 204 | HeadShort string | |
| 202 | 205 | MergedShort string | |
| @@ -469,7 +472,6 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 469 | 472 | State: mrStateName(m), Open: open, Signed: signed, | |
| 470 | 473 | Source: visibleText(m.Source), Target: visibleText(m.Target), | |
| 471 | 474 | Author: s.who(m.Author), | |
| 472 | - | Desc: plainParts(m.Description.String()), | |
| 473 | 475 | Head: head, HeadShort: short(head), | |
| 474 | 476 | NFiles: res.Stats.FilesChanged, | |
| 475 | 477 | Adds: res.Stats.Additions, | |
| @@ -477,6 +479,16 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 477 | 479 | Files: diffFiles(res.Diff.Files), | |
| 478 | 480 | } | |
| 479 | 481 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head)) | |
| 482 | + | if desc := m.Description.String(); strings.TrimSpace(desc) != "" { | |
| 483 | + | // Relative links point into the source branch as it is now (its head | |
| 484 | + | // commit); the heading ids carry their own prefix, apart from the | |
| 485 | + | // ids of the diff. | |
| 486 | + | h, _, err := s.md.renderWithIDs([]byte(desc), d.name(), head, "", "mr-desc-") | |
| 487 | + | if err != nil { | |
| 488 | + | return nil, 0, err | |
| 489 | + | } | |
| 490 | + | p.Desc = h | |
| 491 | + | } | |
| 480 | 492 | p.Opened.Name = p.Author | |
| 481 | 493 | p.Opened.When, p.Opened.WhenAt = whenOf(now, m.CreatedAt) | |
| 482 | 494 | if !m.CreatedAt.IsZero() { | |
| @@ -5,6 +5,7 @@ package web | |||||
| 5 | import ( | 5 | import ( | ||
| 6 | "context" | 6 | "context" | ||
| 7 | "html" | 7 | "html" | ||
| 8 | + | "html/template" | |||
| 8 | "io" | 9 | "io" | ||
| 9 | "net/http" | 10 | "net/http" | ||
| 10 | "net/url" | 11 | "net/url" | ||
| @@ -196,7 +197,9 @@ type MRPage struct { | |||||
| 196 | Opened Person | 197 | Opened Person | ||
| 197 | Updated string | 198 | Updated string | ||
| 198 | UpdatedAt string | 199 | UpdatedAt string | ||
| 199 | - | Desc []TextPart | 200 | + | // Desc is the description rendered as Markdown (as a README: no raw |
| 201 | + | // HTML, filtered URLs), "" when it is empty. | |||
| 202 | + | Desc template.HTML | |||
| 200 | Head string | 203 | Head string | ||
| 201 | HeadShort string | 204 | HeadShort string | ||
| 202 | MergedShort string | 205 | MergedShort string | ||
| @@ -469,7 +472,6 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||||
| 469 | State: mrStateName(m), Open: open, Signed: signed, | 472 | State: mrStateName(m), Open: open, Signed: signed, | ||
| 470 | Source: visibleText(m.Source), Target: visibleText(m.Target), | 473 | Source: visibleText(m.Source), Target: visibleText(m.Target), | ||
| 471 | Author: s.who(m.Author), | 474 | Author: s.who(m.Author), | ||
| 472 | - | Desc: plainParts(m.Description.String()), | |||
| 473 | Head: head, HeadShort: short(head), | 475 | Head: head, HeadShort: short(head), | ||
| 474 | NFiles: res.Stats.FilesChanged, | 476 | NFiles: res.Stats.FilesChanged, | ||
| 475 | Adds: res.Stats.Additions, | 477 | Adds: res.Stats.Additions, | ||
| @@ -477,6 +479,16 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||||
| 477 | Files: diffFiles(res.Diff.Files), | 479 | Files: diffFiles(res.Diff.Files), | ||
| 478 | } | 480 | } | ||
| 479 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head)) | 481 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head)) | ||
| 482 | + | if desc := m.Description.String(); strings.TrimSpace(desc) != "" { | |||
| 483 | + | // Relative links point into the source branch as it is now (its head | |||
| 484 | + | // commit); the heading ids carry their own prefix, apart from the | |||
| 485 | + | // ids of the diff. | |||
| 486 | + | h, _, err := s.md.renderWithIDs([]byte(desc), d.name(), head, "", "mr-desc-") | |||
| 487 | + | if err != nil { | |||
| 488 | + | return nil, 0, err | |||
| 489 | + | } | |||
| 490 | + | p.Desc = h | |||
| 491 | + | } | |||
| 480 | p.Opened.Name = p.Author | 492 | p.Opened.Name = p.Author | ||
| 481 | p.Opened.When, p.Opened.WhenAt = whenOf(now, m.CreatedAt) | 493 | p.Opened.When, p.Opened.WhenAt = whenOf(now, m.CreatedAt) | ||
| 482 | if !m.CreatedAt.IsZero() { | 494 | if !m.CreatedAt.IsZero() { | ||
M internal/adapters/web/mr.templ +5 -3
| @@ -140,8 +140,10 @@ templ MergeRequest(l Layout, p MRPage) { | |||
| 140 | 140 | </dl> | |
| 141 | 141 | </header> | |
| 142 | 142 | @LiveSlot("mr-notice") | |
| 143 | - | if len(p.Desc) > 0 { | |
| 144 | - | <p class="mr-desc">@userText(p.Desc)</p> | |
| 143 | + | if p.Desc != "" { | |
| 144 | + | <article class="mr-desc prose" aria-label="description" hx-ignore> | |
| 145 | + | @templ.Raw(string(p.Desc)) | |
| 146 | + | </article> | |
| 145 | 147 | } | |
| 146 | 148 | if p.Since != nil { | |
| 147 | 149 | <nav class="since" aria-label="Diff range"> | |
| @@ -481,7 +483,7 @@ templ Compare(l Layout, p ComparePage) { | |||
| 481 | 483 | <label for="mr-title">title</label> | |
| 482 | 484 | <input id="mr-title" name="title" type="text" maxlength="120" required value={ p.Create.Title } autocomplete="off"/> | |
| 483 | 485 | <label for="mr-desc">description</label> | |
| 484 | - | <textarea id="mr-desc" name="description" rows="4" maxlength="8000" placeholder="plain text, optional"></textarea> | |
| 486 | + | <textarea id="mr-desc" name="description" rows="4" maxlength="8000" placeholder="Markdown, optional"></textarea> | |
| 485 | 487 | <div class="f-btns"> | |
| 486 | 488 | <button type="submit" class="btn primary">Create merge request</button> | |
| 487 | 489 | </div> | |
| @@ -140,8 +140,10 @@ templ MergeRequest(l Layout, p MRPage) { | |||||
| 140 | </dl> | 140 | </dl> | ||
| 141 | </header> | 141 | </header> | ||
| 142 | @LiveSlot("mr-notice") | 142 | @LiveSlot("mr-notice") | ||
| 143 | - | if len(p.Desc) > 0 { | 143 | + | if p.Desc != "" { |
| 144 | - | <p class="mr-desc">@userText(p.Desc)</p> | 144 | + | <article class="mr-desc prose" aria-label="description" hx-ignore> |
| 145 | + | @templ.Raw(string(p.Desc)) | |||
| 146 | + | </article> | |||
| 145 | } | 147 | } | ||
| 146 | if p.Since != nil { | 148 | if p.Since != nil { | ||
| 147 | <nav class="since" aria-label="Diff range"> | 149 | <nav class="since" aria-label="Diff range"> | ||
| @@ -481,7 +483,7 @@ templ Compare(l Layout, p ComparePage) { | |||||
| 481 | <label for="mr-title">title</label> | 483 | <label for="mr-title">title</label> | ||
| 482 | <input id="mr-title" name="title" type="text" maxlength="120" required value={ p.Create.Title } autocomplete="off"/> | 484 | <input id="mr-title" name="title" type="text" maxlength="120" required value={ p.Create.Title } autocomplete="off"/> | ||
| 483 | <label for="mr-desc">description</label> | 485 | <label for="mr-desc">description</label> | ||
| 484 | - | <textarea id="mr-desc" name="description" rows="4" maxlength="8000" placeholder="plain text, optional"></textarea> | 486 | + | <textarea id="mr-desc" name="description" rows="4" maxlength="8000" placeholder="Markdown, optional"></textarea> |
| 485 | <div class="f-btns"> | 487 | <div class="f-btns"> | ||
| 486 | <button type="submit" class="btn primary">Create merge request</button> | 488 | <button type="submit" class="btn primary">Create merge request</button> | ||
| 487 | </div> | 489 | </div> | ||
M internal/adapters/web/mr_templ.go +100 -100 generated file: collapsed
M internal/adapters/web/pages.templ +3 -2
| @@ -68,10 +68,11 @@ templ latestTagFact(t LatestTag) { | |||
| 68 | 68 | <dt>latest tag</dt> | |
| 69 | 69 | <dd class="latest-tag"> | |
| 70 | 70 | if t.Name != "" { | |
| 71 | - | <a class="tag-name" href={ templ.SafeURL(t.Href) } title={ t.Subject }><span class="ico i-tag" aria-hidden="true"></span>{ t.Name }</a> | |
| 71 | + | <a class="tag-name" href={ templ.SafeURL(t.Href) } title={ t.Subject }>{ t.Name }</a> | |
| 72 | 72 | if t.WhenAt != "" { | |
| 73 | - | <time class="dim" datetime={ t.WhenAt } title={ t.WhenAt }>{ t.When }</time> | |
| 73 | + | { " · " }<time datetime={ t.WhenAt } title={ t.WhenAt }>{ t.When }</time> | |
| 74 | 74 | } | |
| 75 | + | { " · " } | |
| 75 | 76 | } | |
| 76 | 77 | <a class="all-tags" href={ templ.SafeURL(t.RefsHref) }>{ plural(t.Count, "tag", "tags") }</a> | |
| 77 | 78 | </dd> | |
| @@ -68,10 +68,11 @@ templ latestTagFact(t LatestTag) { | |||||
| 68 | <dt>latest tag</dt> | 68 | <dt>latest tag</dt> | ||
| 69 | <dd class="latest-tag"> | 69 | <dd class="latest-tag"> | ||
| 70 | if t.Name != "" { | 70 | if t.Name != "" { | ||
| 71 | - | <a class="tag-name" href={ templ.SafeURL(t.Href) } title={ t.Subject }><span class="ico i-tag" aria-hidden="true"></span>{ t.Name }</a> | 71 | + | <a class="tag-name" href={ templ.SafeURL(t.Href) } title={ t.Subject }>{ t.Name }</a> |
| 72 | if t.WhenAt != "" { | 72 | if t.WhenAt != "" { | ||
| 73 | - | <time class="dim" datetime={ t.WhenAt } title={ t.WhenAt }>{ t.When }</time> | 73 | + | { " · " }<time datetime={ t.WhenAt } title={ t.WhenAt }>{ t.When }</time> |
| 74 | } | 74 | } | ||
| 75 | + | { " · " } | |||
| 75 | } | 76 | } | ||
| 76 | <a class="all-tags" href={ templ.SafeURL(t.RefsHref) }>{ plural(t.Count, "tag", "tags") }</a> | 77 | <a class="all-tags" href={ templ.SafeURL(t.RefsHref) }>{ plural(t.Count, "tag", "tags") }</a> | ||
| 77 | </dd> | 78 | </dd> | ||
M internal/adapters/web/pages_templ.go +325 -299 generated file: collapsed
M internal/adapters/web/shell.templ +0 -10
| @@ -128,16 +128,6 @@ templ Header(l Layout) { | |||
| 128 | 128 | <span class="search-label">Go to…</span> | |
| 129 | 129 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 130 | 130 | </button> | |
| 131 | - | <button | |
| 132 | - | type="button" | |
| 133 | - | class="btn icon" | |
| 134 | - | data-install | |
| 135 | - | hidden | |
| 136 | - | aria-label="Install app" | |
| 137 | - | title="install ermite as an app" | |
| 138 | - | > | |
| 139 | - | <span class="ico i-download" aria-hidden="true"></span> | |
| 140 | - | </button> | |
| 141 | 131 | if SignedIn(ctx) { | |
| 142 | 132 | <form class="owner" method="post" action="/-/logout"> | |
| 143 | 133 | <span class="owner-badge" title="signed in as the owner"><span class="ico i-user" aria-hidden="true"></span>{ l.Owner }</span> | |
| @@ -128,16 +128,6 @@ templ Header(l Layout) { | |||||
| 128 | <span class="search-label">Go to…</span> | 128 | <span class="search-label">Go to…</span> | ||
| 129 | <kbd aria-hidden="true">ctrl k</kbd> | 129 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 130 | </button> | 130 | </button> | ||
| 131 | - | <button | |||
| 132 | - | type="button" | |||
| 133 | - | class="btn icon" | |||
| 134 | - | data-install | |||
| 135 | - | hidden | |||
| 136 | - | aria-label="Install app" | |||
| 137 | - | title="install ermite as an app" | |||
| 138 | - | > | |||
| 139 | - | <span class="ico i-download" aria-hidden="true"></span> | |||
| 140 | - | </button> | |||
| 141 | if SignedIn(ctx) { | 131 | if SignedIn(ctx) { | ||
| 142 | <form class="owner" method="post" action="/-/logout"> | 132 | <form class="owner" method="post" action="/-/logout"> | ||
| 143 | <span class="owner-badge" title="signed in as the owner"><span class="ico i-user" aria-hidden="true"></span>{ l.Owner }</span> | 133 | <span class="owner-badge" title="signed in as the owner"><span class="ico i-user" aria-hidden="true"></span>{ l.Owner }</span> | ||
M internal/adapters/web/shell_templ.go +11 -11
| @@ -565,7 +565,7 @@ func Header(l Layout) templ.Component { | |||
| 565 | 565 | return templ_7745c5c3_Err | |
| 566 | 566 | } | |
| 567 | 567 | } | |
| 568 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> <button type=\"button\" class=\"btn icon\" data-install hidden aria-label=\"Install app\" title=\"install ermite as an app\"><span class=\"ico i-download\" aria-hidden=\"true\"></span></button> ") | |
| 568 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ") | |
| 569 | 569 | if templ_7745c5c3_Err != nil { | |
| 570 | 570 | return templ_7745c5c3_Err | |
| 571 | 571 | } | |
| @@ -577,7 +577,7 @@ func Header(l Layout) templ.Component { | |||
| 577 | 577 | var templ_7745c5c3_Var30 string | |
| 578 | 578 | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner) | |
| 579 | 579 | if templ_7745c5c3_Err != nil { | |
| 580 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 143, Col: 123} | |
| 580 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 123} | |
| 581 | 581 | } | |
| 582 | 582 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | |
| 583 | 583 | if templ_7745c5c3_Err != nil { | |
| @@ -620,7 +620,7 @@ func Header(l Layout) templ.Component { | |||
| 620 | 620 | var templ_7745c5c3_Var31 templ.SafeURL | |
| 621 | 621 | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href)) | |
| 622 | 622 | if templ_7745c5c3_Err != nil { | |
| 623 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 160, Col: 36} | |
| 623 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 150, Col: 36} | |
| 624 | 624 | } | |
| 625 | 625 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 626 | 626 | if templ_7745c5c3_Err != nil { | |
| @@ -633,7 +633,7 @@ func Header(l Layout) templ.Component { | |||
| 633 | 633 | var templ_7745c5c3_Var32 string | |
| 634 | 634 | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | |
| 635 | 635 | if templ_7745c5c3_Err != nil { | |
| 636 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 161, Col: 24} | |
| 636 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 151, Col: 24} | |
| 637 | 637 | } | |
| 638 | 638 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) | |
| 639 | 639 | if templ_7745c5c3_Err != nil { | |
| @@ -646,7 +646,7 @@ func Header(l Layout) templ.Component { | |||
| 646 | 646 | var templ_7745c5c3_Var33 string | |
| 647 | 647 | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | |
| 648 | 648 | if templ_7745c5c3_Err != nil { | |
| 649 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 162, Col: 33} | |
| 649 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 152, Col: 33} | |
| 650 | 650 | } | |
| 651 | 651 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) | |
| 652 | 652 | if templ_7745c5c3_Err != nil { | |
| @@ -669,7 +669,7 @@ func Header(l Layout) templ.Component { | |||
| 669 | 669 | var templ_7745c5c3_Var34 string | |
| 670 | 670 | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label) | |
| 671 | 671 | if templ_7745c5c3_Err != nil { | |
| 672 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 167, Col: 17} | |
| 672 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 157, Col: 17} | |
| 673 | 673 | } | |
| 674 | 674 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | |
| 675 | 675 | if templ_7745c5c3_Err != nil { | |
| @@ -687,7 +687,7 @@ func Header(l Layout) templ.Component { | |||
| 687 | 687 | var templ_7745c5c3_Var35 string | |
| 688 | 688 | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count) | |
| 689 | 689 | if templ_7745c5c3_Err != nil { | |
| 690 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 169, Col: 38} | |
| 690 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 159, Col: 38} | |
| 691 | 691 | } | |
| 692 | 692 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 693 | 693 | if templ_7745c5c3_Err != nil { | |
| @@ -700,7 +700,7 @@ func Header(l Layout) templ.Component { | |||
| 700 | 700 | var templ_7745c5c3_Var36 string | |
| 701 | 701 | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(" open") | |
| 702 | 702 | if templ_7745c5c3_Err != nil { | |
| 703 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 169, Col: 66} | |
| 703 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 159, Col: 66} | |
| 704 | 704 | } | |
| 705 | 705 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | |
| 706 | 706 | if templ_7745c5c3_Err != nil { | |
| @@ -718,7 +718,7 @@ func Header(l Layout) templ.Component { | |||
| 718 | 718 | var templ_7745c5c3_Var37 string | |
| 719 | 719 | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key) | |
| 720 | 720 | if templ_7745c5c3_Err != nil { | |
| 721 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 171, Col: 39} | |
| 721 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 161, Col: 39} | |
| 722 | 722 | } | |
| 723 | 723 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) | |
| 724 | 724 | if templ_7745c5c3_Err != nil { | |
| @@ -772,7 +772,7 @@ func Footer(l Layout) templ.Component { | |||
| 772 | 772 | var templ_7745c5c3_Var39 string | |
| 773 | 773 | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | |
| 774 | 774 | if templ_7745c5c3_Err != nil { | |
| 775 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 185, Col: 26} | |
| 775 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 175, Col: 26} | |
| 776 | 776 | } | |
| 777 | 777 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | |
| 778 | 778 | if templ_7745c5c3_Err != nil { | |
| @@ -785,7 +785,7 @@ func Footer(l Layout) templ.Component { | |||
| 785 | 785 | var templ_7745c5c3_Var40 templ.SafeURL | |
| 786 | 786 | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | |
| 787 | 787 | if templ_7745c5c3_Err != nil { | |
| 788 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 186, Col: 38} | |
| 788 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 38} | |
| 789 | 789 | } | |
| 790 | 790 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | |
| 791 | 791 | if templ_7745c5c3_Err != nil { | |
| @@ -565,7 +565,7 @@ func Header(l Layout) templ.Component { | |||||
| 565 | return templ_7745c5c3_Err | 565 | return templ_7745c5c3_Err | ||
| 566 | } | 566 | } | ||
| 567 | } | 567 | } | ||
| 568 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> <button type=\"button\" class=\"btn icon\" data-install hidden aria-label=\"Install app\" title=\"install ermite as an app\"><span class=\"ico i-download\" aria-hidden=\"true\"></span></button> ") | 568 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ") |
| 569 | if templ_7745c5c3_Err != nil { | 569 | if templ_7745c5c3_Err != nil { | ||
| 570 | return templ_7745c5c3_Err | 570 | return templ_7745c5c3_Err | ||
| 571 | } | 571 | } | ||
| @@ -577,7 +577,7 @@ func Header(l Layout) templ.Component { | |||||
| 577 | var templ_7745c5c3_Var30 string | 577 | var templ_7745c5c3_Var30 string | ||
| 578 | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner) | 578 | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner) | ||
| 579 | if templ_7745c5c3_Err != nil { | 579 | if templ_7745c5c3_Err != nil { | ||
| 580 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 143, Col: 123} | 580 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 123} |
| 581 | } | 581 | } | ||
| 582 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | 582 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | ||
| 583 | if templ_7745c5c3_Err != nil { | 583 | if templ_7745c5c3_Err != nil { | ||
| @@ -620,7 +620,7 @@ func Header(l Layout) templ.Component { | |||||
| 620 | var templ_7745c5c3_Var31 templ.SafeURL | 620 | var templ_7745c5c3_Var31 templ.SafeURL | ||
| 621 | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href)) | 621 | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href)) | ||
| 622 | if templ_7745c5c3_Err != nil { | 622 | if templ_7745c5c3_Err != nil { | ||
| 623 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 160, Col: 36} | 623 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 150, Col: 36} |
| 624 | } | 624 | } | ||
| 625 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | 625 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | ||
| 626 | if templ_7745c5c3_Err != nil { | 626 | if templ_7745c5c3_Err != nil { | ||
| @@ -633,7 +633,7 @@ func Header(l Layout) templ.Component { | |||||
| 633 | var templ_7745c5c3_Var32 string | 633 | var templ_7745c5c3_Var32 string | ||
| 634 | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | 634 | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | ||
| 635 | if templ_7745c5c3_Err != nil { | 635 | if templ_7745c5c3_Err != nil { | ||
| 636 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 161, Col: 24} | 636 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 151, Col: 24} |
| 637 | } | 637 | } | ||
| 638 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) | 638 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) | ||
| 639 | if templ_7745c5c3_Err != nil { | 639 | if templ_7745c5c3_Err != nil { | ||
| @@ -646,7 +646,7 @@ func Header(l Layout) templ.Component { | |||||
| 646 | var templ_7745c5c3_Var33 string | 646 | var templ_7745c5c3_Var33 string | ||
| 647 | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | 647 | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | ||
| 648 | if templ_7745c5c3_Err != nil { | 648 | if templ_7745c5c3_Err != nil { | ||
| 649 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 162, Col: 33} | 649 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 152, Col: 33} |
| 650 | } | 650 | } | ||
| 651 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) | 651 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) | ||
| 652 | if templ_7745c5c3_Err != nil { | 652 | if templ_7745c5c3_Err != nil { | ||
| @@ -669,7 +669,7 @@ func Header(l Layout) templ.Component { | |||||
| 669 | var templ_7745c5c3_Var34 string | 669 | var templ_7745c5c3_Var34 string | ||
| 670 | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label) | 670 | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label) | ||
| 671 | if templ_7745c5c3_Err != nil { | 671 | if templ_7745c5c3_Err != nil { | ||
| 672 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 167, Col: 17} | 672 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 157, Col: 17} |
| 673 | } | 673 | } | ||
| 674 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | 674 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | ||
| 675 | if templ_7745c5c3_Err != nil { | 675 | if templ_7745c5c3_Err != nil { | ||
| @@ -687,7 +687,7 @@ func Header(l Layout) templ.Component { | |||||
| 687 | var templ_7745c5c3_Var35 string | 687 | var templ_7745c5c3_Var35 string | ||
| 688 | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count) | 688 | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count) | ||
| 689 | if templ_7745c5c3_Err != nil { | 689 | if templ_7745c5c3_Err != nil { | ||
| 690 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 169, Col: 38} | 690 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 159, Col: 38} |
| 691 | } | 691 | } | ||
| 692 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | 692 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | ||
| 693 | if templ_7745c5c3_Err != nil { | 693 | if templ_7745c5c3_Err != nil { | ||
| @@ -700,7 +700,7 @@ func Header(l Layout) templ.Component { | |||||
| 700 | var templ_7745c5c3_Var36 string | 700 | var templ_7745c5c3_Var36 string | ||
| 701 | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(" open") | 701 | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(" open") | ||
| 702 | if templ_7745c5c3_Err != nil { | 702 | if templ_7745c5c3_Err != nil { | ||
| 703 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 169, Col: 66} | 703 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 159, Col: 66} |
| 704 | } | 704 | } | ||
| 705 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | 705 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | ||
| 706 | if templ_7745c5c3_Err != nil { | 706 | if templ_7745c5c3_Err != nil { | ||
| @@ -718,7 +718,7 @@ func Header(l Layout) templ.Component { | |||||
| 718 | var templ_7745c5c3_Var37 string | 718 | var templ_7745c5c3_Var37 string | ||
| 719 | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key) | 719 | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key) | ||
| 720 | if templ_7745c5c3_Err != nil { | 720 | if templ_7745c5c3_Err != nil { | ||
| 721 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 171, Col: 39} | 721 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 161, Col: 39} |
| 722 | } | 722 | } | ||
| 723 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) | 723 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) | ||
| 724 | if templ_7745c5c3_Err != nil { | 724 | if templ_7745c5c3_Err != nil { | ||
| @@ -772,7 +772,7 @@ func Footer(l Layout) templ.Component { | |||||
| 772 | var templ_7745c5c3_Var39 string | 772 | var templ_7745c5c3_Var39 string | ||
| 773 | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | 773 | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | ||
| 774 | if templ_7745c5c3_Err != nil { | 774 | if templ_7745c5c3_Err != nil { | ||
| 775 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 185, Col: 26} | 775 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 175, Col: 26} |
| 776 | } | 776 | } | ||
| 777 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | 777 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | ||
| 778 | if templ_7745c5c3_Err != nil { | 778 | if templ_7745c5c3_Err != nil { | ||
| @@ -785,7 +785,7 @@ func Footer(l Layout) templ.Component { | |||||
| 785 | var templ_7745c5c3_Var40 templ.SafeURL | 785 | var templ_7745c5c3_Var40 templ.SafeURL | ||
| 786 | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | 786 | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | ||
| 787 | if templ_7745c5c3_Err != nil { | 787 | if templ_7745c5c3_Err != nil { | ||
| 788 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 186, Col: 38} | 788 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 38} |
| 789 | } | 789 | } | ||
| 790 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | 790 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | ||
| 791 | if templ_7745c5c3_Err != nil { | 791 | if templ_7745c5c3_Err != nil { | ||
M internal/adapters/web/static/css/20-shell.css +5 -9
| @@ -26,7 +26,6 @@ | |||
| 26 | 26 | .i-key { --i: url(icons/key.svg); } | |
| 27 | 27 | .i-log-in { --i: url(icons/log-in.svg); } | |
| 28 | 28 | .i-log-out { --i: url(icons/log-out.svg); } | |
| 29 | - | .i-download { --i: url(icons/download.svg); } | |
| 30 | 29 | ||
| 31 | 30 | .pill { | |
| 32 | 31 | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | |
| @@ -135,15 +134,12 @@ | |||
| 135 | 134 | .rail section > .list { margin: var(--s2) 0; } | |
| 136 | 135 | .rail section > p + p { margin-top: calc(-1 * var(--s2)); } | |
| 137 | 136 | ||
| 138 | - | /* newest tag, a row of About in the summary rail */ | |
| 137 | + | /* newest tag, a row of About in the summary rail: one line of text like | |
| 138 | + | the rows above it ("v0.9.0 · 36h ago · 9 tags") */ | |
| 139 | 139 | .kv dd.latest-tag { | |
| 140 | - | display: flex; align-items: baseline; gap: var(--s2); | |
| 141 | - | & .tag-name { display: inline-flex; align-items: center; gap: 4px; min-width: 0; overflow: hidden; font-weight: 700; font-family: var(--mono); color: var(--ink-hi); text-decoration: none; } | |
| 142 | - | & .tag-name .ico { flex: none; } | |
| 143 | - | & .tag-name:hover { text-decoration: underline; } | |
| 144 | - | & time { white-space: nowrap; } | |
| 145 | - | & .all-tags { margin-left: auto; white-space: nowrap; color: var(--ink-dim); text-decoration: none; } | |
| 146 | - | & .all-tags:hover { color: var(--ink-hi); text-decoration: underline; } | |
| 140 | + | & a { color: inherit; text-decoration: none; } | |
| 141 | + | & .tag-name { font-weight: 700; color: var(--ink-hi); } | |
| 142 | + | & a:hover { color: var(--ink-hi); text-decoration: underline; } | |
| 147 | 143 | } | |
| 148 | 144 | ||
| 149 | 145 | .kv { | |
| @@ -26,7 +26,6 @@ | |||||
| 26 | .i-key { --i: url(icons/key.svg); } | 26 | .i-key { --i: url(icons/key.svg); } | ||
| 27 | .i-log-in { --i: url(icons/log-in.svg); } | 27 | .i-log-in { --i: url(icons/log-in.svg); } | ||
| 28 | .i-log-out { --i: url(icons/log-out.svg); } | 28 | .i-log-out { --i: url(icons/log-out.svg); } | ||
| 29 | - | .i-download { --i: url(icons/download.svg); } | |||
| 30 | 29 | ||||
| 31 | .pill { | 30 | .pill { | ||
| 32 | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | 31 | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | ||
| @@ -135,15 +134,12 @@ | |||||
| 135 | .rail section > .list { margin: var(--s2) 0; } | 134 | .rail section > .list { margin: var(--s2) 0; } | ||
| 136 | .rail section > p + p { margin-top: calc(-1 * var(--s2)); } | 135 | .rail section > p + p { margin-top: calc(-1 * var(--s2)); } | ||
| 137 | 136 | ||||
| 138 | - | /* newest tag, a row of About in the summary rail */ | 137 | + | /* newest tag, a row of About in the summary rail: one line of text like |
| 138 | + | the rows above it ("v0.9.0 · 36h ago · 9 tags") */ | |||
| 139 | .kv dd.latest-tag { | 139 | .kv dd.latest-tag { | ||
| 140 | - | display: flex; align-items: baseline; gap: var(--s2); | 140 | + | & a { color: inherit; text-decoration: none; } |
| 141 | - | & .tag-name { display: inline-flex; align-items: center; gap: 4px; min-width: 0; overflow: hidden; font-weight: 700; font-family: var(--mono); color: var(--ink-hi); text-decoration: none; } | 141 | + | & .tag-name { font-weight: 700; color: var(--ink-hi); } |
| 142 | - | & .tag-name .ico { flex: none; } | 142 | + | & a:hover { color: var(--ink-hi); text-decoration: underline; } |
| 143 | - | & .tag-name:hover { text-decoration: underline; } | |||
| 144 | - | & time { white-space: nowrap; } | |||
| 145 | - | & .all-tags { margin-left: auto; white-space: nowrap; color: var(--ink-dim); text-decoration: none; } | |||
| 146 | - | & .all-tags:hover { color: var(--ink-hi); text-decoration: underline; } | |||
| 147 | } | 143 | } | ||
| 148 | 144 | ||||
| 149 | .kv { | 145 | .kv { | ||
M internal/adapters/web/static/css/60-mr.css +6 -2
| @@ -6,8 +6,12 @@ | |||
| 6 | 6 | .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); } | |
| 7 | 7 | .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); } | |
| 8 | 8 | .mr-state[data-state="draft"]::before { content: "◌ "; color: var(--ink-dim); } | |
| 9 | - | .mr-desc { max-width: var(--measure); white-space: pre-wrap; overflow-wrap: anywhere; padding: var(--s4) var(--s5); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); } | |
| 10 | - | .mr-desc a, .c-text a { text-decoration: underline; } | |
| 9 | + | /* the description: Markdown as a README, as wide as the header card above; | |
| 10 | + | the text inside keeps the reading measure */ | |
| 11 | + | .mr-desc.prose { max-width: none; padding: var(--s4) var(--s5); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); } | |
| 12 | + | .mr-desc.prose > * { max-width: var(--measure); } | |
| 13 | + | .mr-desc.prose > pre, .mr-desc.prose > table { max-width: 100%; } | |
| 14 | + | .c-text a { text-decoration: underline; } | |
| 11 | 15 | ||
| 12 | 16 | .since { | |
| 13 | 17 | display: inline-flex; flex-wrap: wrap; gap: var(--s1); padding: var(--s1); | |
| @@ -6,8 +6,12 @@ | |||||
| 6 | .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); } | 6 | .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); } | ||
| 7 | .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); } | 7 | .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); } | ||
| 8 | .mr-state[data-state="draft"]::before { content: "◌ "; color: var(--ink-dim); } | 8 | .mr-state[data-state="draft"]::before { content: "◌ "; color: var(--ink-dim); } | ||
| 9 | - | .mr-desc { max-width: var(--measure); white-space: pre-wrap; overflow-wrap: anywhere; padding: var(--s4) var(--s5); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); } | 9 | + | /* the description: Markdown as a README, as wide as the header card above; |
| 10 | - | .mr-desc a, .c-text a { text-decoration: underline; } | 10 | + | the text inside keeps the reading measure */ |
| 11 | + | .mr-desc.prose { max-width: none; padding: var(--s4) var(--s5); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); } | |||
| 12 | + | .mr-desc.prose > * { max-width: var(--measure); } | |||
| 13 | + | .mr-desc.prose > pre, .mr-desc.prose > table { max-width: 100%; } | |||
| 14 | + | .c-text a { text-decoration: underline; } | |||
| 11 | 15 | ||||
| 12 | .since { | 16 | .since { | ||
| 13 | display: inline-flex; flex-wrap: wrap; gap: var(--s1); padding: var(--s1); | 17 | display: inline-flex; flex-wrap: wrap; gap: var(--s1); padding: var(--s1); | ||
D internal/adapters/web/static/icons/download.svg +0 -1
| @@ -1,1 +0,0 @@ | |||
| 1 | - | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/></svg> | |
| @@ -1,1 +0,0 @@ | |||||
| 1 | - | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/></svg> | |||
M internal/adapters/web/static/js/ermite.js +6 -51
| @@ -11,7 +11,7 @@ | |||
| 11 | 11 | // 4. The file of the diff in view is marked in the table of contents. | |
| 12 | 12 | // 5. A handler that fails is shown in the hint badge. | |
| 13 | 13 | // 6. The lines a diff hides load in place (ermiteGaps; e and E). | |
| 14 | - | // 7. The service worker and the "install app" button (ermiteInstall). | |
| 14 | + | // 7. The service worker (ermiteInstall). | |
| 15 | 15 | (() => { | |
| 16 | 16 | // Marks the page as scripted: what does nothing without scripts (the filters, | |
| 17 | 17 | // the "go to" and help buttons) is hidden until this class is set (see the | |
| @@ -366,20 +366,12 @@ | |||
| 366 | 366 | }; | |
| 367 | 367 | })(); | |
| 368 | 368 | ||
| 369 | - | // The installed app. The service worker (/-/sw.js) is registered once the page | |
| 370 | - | // has loaded. Chrome offers the install by itself only as an icon in the | |
| 371 | - | // address bar of a desktop (or a mini-bar on a phone): the page keeps the | |
| 372 | - | // beforeinstallprompt event and shows its own button ([data-install] in the | |
| 373 | - | // header), which asks the browser to prompt. The button stays hidden when the | |
| 374 | - | // app is already running installed or once it was installed. Self-contained: | |
| 375 | - | // jstest/install.test.mjs evaluates this block alone. | |
| 369 | + | // The installed app: the service worker (/-/sw.js) is registered once the | |
| 370 | + | // page has loaded. With it and the manifest the browser offers the install | |
| 371 | + | // by itself (the address bar of a desktop, the menu or a banner on a phone); | |
| 372 | + | // the page leaves that offer alone. Self-contained: jstest/install.test.mjs | |
| 373 | + | // evaluates this block alone. | |
| 376 | 374 | window.ermiteInstall = (() => { | |
| 377 | - | let deferred = null; | |
| 378 | - | const buttons = () => $$('[data-install]'); | |
| 379 | - | const show = (on) => { for (const b of buttons()) b.hidden = !on; }; | |
| 380 | - | const installed = () => | |
| 381 | - | !!window.matchMedia?.('(display-mode: standalone), (display-mode: minimal-ui), (display-mode: window-controls-overlay)').matches || | |
| 382 | - | navigator.standalone === true; | |
| 383 | 375 | const api = { | |
| 384 | 376 | // register installs the service worker; a browser without one, or a | |
| 385 | 377 | // refusal, leaves the site working as before. | |
| @@ -387,44 +379,7 @@ | |||
| 387 | 379 | if (!navigator.serviceWorker) return Promise.resolve(null); | |
| 388 | 380 | return navigator.serviceWorker.register('/-/sw.js', { scope: '/' }).catch(() => null); | |
| 389 | 381 | }, | |
| 390 | - | // offer keeps the event the browser sent and shows the button. | |
| 391 | - | offer(e) { | |
| 392 | - | if (installed()) return; | |
| 393 | - | e.preventDefault(); | |
| 394 | - | deferred = e; | |
| 395 | - | show(true); | |
| 396 | - | }, | |
| 397 | - | // prompt asks the browser to show its install dialog. The event works | |
| 398 | - | // once: a new one comes if the visitor dismisses the dialog. The button | |
| 399 | - | // stays while the dialog is up; once it hides, the focus it held moves | |
| 400 | - | // to the search of the header rather than to the body. | |
| 401 | - | async prompt() { | |
| 402 | - | const e = deferred; | |
| 403 | - | if (!e) return ''; | |
| 404 | - | deferred = null; | |
| 405 | - | let outcome = ''; | |
| 406 | - | try { | |
| 407 | - | await e.prompt(); | |
| 408 | - | outcome = (await e.userChoice).outcome; | |
| 409 | - | } catch { | |
| 410 | - | outcome = ''; | |
| 411 | - | } | |
| 412 | - | const had = buttons().some((b) => b === document.activeElement); | |
| 413 | - | show(false); | |
| 414 | - | if (had) document.querySelector('[aria-controls="panel-cmd"]')?.focus?.(); | |
| 415 | - | return outcome; | |
| 416 | - | }, | |
| 417 | - | done() { | |
| 418 | - | deferred = null; | |
| 419 | - | show(false); | |
| 420 | - | }, | |
| 421 | 382 | }; | |
| 422 | - | addEventListener('beforeinstallprompt', api.offer); | |
| 423 | - | addEventListener('appinstalled', api.done); | |
| 424 | - | addEventListener('click', (e) => { | |
| 425 | - | // pending is the prompt the last click started (its outcome) | |
| 426 | - | if (e.target.closest?.('[data-install]')) api.pending = api.prompt(); | |
| 427 | - | }); | |
| 428 | 383 | if (document.readyState === 'complete') api.register(); | |
| 429 | 384 | else addEventListener('load', () => api.register(), { once: true }); | |
| 430 | 385 | return api; | |
| @@ -11,7 +11,7 @@ | |||||
| 11 | // 4. The file of the diff in view is marked in the table of contents. | 11 | // 4. The file of the diff in view is marked in the table of contents. | ||
| 12 | // 5. A handler that fails is shown in the hint badge. | 12 | // 5. A handler that fails is shown in the hint badge. | ||
| 13 | // 6. The lines a diff hides load in place (ermiteGaps; e and E). | 13 | // 6. The lines a diff hides load in place (ermiteGaps; e and E). | ||
| 14 | - | // 7. The service worker and the "install app" button (ermiteInstall). | 14 | + | // 7. The service worker (ermiteInstall). |
| 15 | (() => { | 15 | (() => { | ||
| 16 | // Marks the page as scripted: what does nothing without scripts (the filters, | 16 | // Marks the page as scripted: what does nothing without scripts (the filters, | ||
| 17 | // the "go to" and help buttons) is hidden until this class is set (see the | 17 | // the "go to" and help buttons) is hidden until this class is set (see the | ||
| @@ -366,20 +366,12 @@ | |||||
| 366 | }; | 366 | }; | ||
| 367 | })(); | 367 | })(); | ||
| 368 | 368 | ||||
| 369 | - | // The installed app. The service worker (/-/sw.js) is registered once the page | 369 | + | // The installed app: the service worker (/-/sw.js) is registered once the |
| 370 | - | // has loaded. Chrome offers the install by itself only as an icon in the | 370 | + | // page has loaded. With it and the manifest the browser offers the install |
| 371 | - | // address bar of a desktop (or a mini-bar on a phone): the page keeps the | 371 | + | // by itself (the address bar of a desktop, the menu or a banner on a phone); |
| 372 | - | // beforeinstallprompt event and shows its own button ([data-install] in the | 372 | + | // the page leaves that offer alone. Self-contained: jstest/install.test.mjs |
| 373 | - | // header), which asks the browser to prompt. The button stays hidden when the | 373 | + | // evaluates this block alone. |
| 374 | - | // app is already running installed or once it was installed. Self-contained: | |||
| 375 | - | // jstest/install.test.mjs evaluates this block alone. | |||
| 376 | window.ermiteInstall = (() => { | 374 | window.ermiteInstall = (() => { | ||
| 377 | - | let deferred = null; | |||
| 378 | - | const buttons = () => $$('[data-install]'); | |||
| 379 | - | const show = (on) => { for (const b of buttons()) b.hidden = !on; }; | |||
| 380 | - | const installed = () => | |||
| 381 | - | !!window.matchMedia?.('(display-mode: standalone), (display-mode: minimal-ui), (display-mode: window-controls-overlay)').matches || | |||
| 382 | - | navigator.standalone === true; | |||
| 383 | const api = { | 375 | const api = { | ||
| 384 | // register installs the service worker; a browser without one, or a | 376 | // register installs the service worker; a browser without one, or a | ||
| 385 | // refusal, leaves the site working as before. | 377 | // refusal, leaves the site working as before. | ||
| @@ -387,44 +379,7 @@ | |||||
| 387 | if (!navigator.serviceWorker) return Promise.resolve(null); | 379 | if (!navigator.serviceWorker) return Promise.resolve(null); | ||
| 388 | return navigator.serviceWorker.register('/-/sw.js', { scope: '/' }).catch(() => null); | 380 | return navigator.serviceWorker.register('/-/sw.js', { scope: '/' }).catch(() => null); | ||
| 389 | }, | 381 | }, | ||
| 390 | - | // offer keeps the event the browser sent and shows the button. | |||
| 391 | - | offer(e) { | |||
| 392 | - | if (installed()) return; | |||
| 393 | - | e.preventDefault(); | |||
| 394 | - | deferred = e; | |||
| 395 | - | show(true); | |||
| 396 | - | }, | |||
| 397 | - | // prompt asks the browser to show its install dialog. The event works | |||
| 398 | - | // once: a new one comes if the visitor dismisses the dialog. The button | |||
| 399 | - | // stays while the dialog is up; once it hides, the focus it held moves | |||
| 400 | - | // to the search of the header rather than to the body. | |||
| 401 | - | async prompt() { | |||
| 402 | - | const e = deferred; | |||
| 403 | - | if (!e) return ''; | |||
| 404 | - | deferred = null; | |||
| 405 | - | let outcome = ''; | |||
| 406 | - | try { | |||
| 407 | - | await e.prompt(); | |||
| 408 | - | outcome = (await e.userChoice).outcome; | |||
| 409 | - | } catch { | |||
| 410 | - | outcome = ''; | |||
| 411 | - | } | |||
| 412 | - | const had = buttons().some((b) => b === document.activeElement); | |||
| 413 | - | show(false); | |||
| 414 | - | if (had) document.querySelector('[aria-controls="panel-cmd"]')?.focus?.(); | |||
| 415 | - | return outcome; | |||
| 416 | - | }, | |||
| 417 | - | done() { | |||
| 418 | - | deferred = null; | |||
| 419 | - | show(false); | |||
| 420 | - | }, | |||
| 421 | }; | 382 | }; | ||
| 422 | - | addEventListener('beforeinstallprompt', api.offer); | |||
| 423 | - | addEventListener('appinstalled', api.done); | |||
| 424 | - | addEventListener('click', (e) => { | |||
| 425 | - | // pending is the prompt the last click started (its outcome) | |||
| 426 | - | if (e.target.closest?.('[data-install]')) api.pending = api.prompt(); | |||
| 427 | - | }); | |||
| 428 | if (document.readyState === 'complete') api.register(); | 383 | if (document.readyState === 'complete') api.register(); | ||
| 429 | else addEventListener('load', () => api.register(), { once: true }); | 384 | else addEventListener('load', () => api.register(), { once: true }); | ||
| 430 | return api; | 385 | return api; | ||
M internal/app/testdata/web/blob-binary.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/blob-code.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/blob-empty.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/blob-image.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/blob-markdown.html +0 -4
| @@ -69,10 +69,6 @@ | |||
| 69 | 69 | <span class="search-label">Go to…</span> | |
| 70 | 70 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 71 | 71 | </button> | |
| 72 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 73 | - | <span class="ico i-download" aria-hidden="true"> | |
| 74 | - | </span> | |
| 75 | - | </button> | |
| 76 | 72 | </div> | |
| 77 | 73 | </div> | |
| 78 | 74 | <nav class="tabs" aria-label="Sections"> | |
| @@ -69,10 +69,6 @@ | |||||
| 69 | <span class="search-label">Go to…</span> | 69 | <span class="search-label">Go to…</span> | ||
| 70 | <kbd aria-hidden="true">ctrl k</kbd> | 70 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 71 | </button> | 71 | </button> | ||
| 72 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 73 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 74 | - | </span> | |||
| 75 | - | </button> | |||
| 76 | </div> | 72 | </div> | ||
| 77 | </div> | 73 | </div> | ||
| 78 | <nav class="tabs" aria-label="Sections"> | 74 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/blob-svg.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/blob-text.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/blob-too-large.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/blob-trojan.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commit-big.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commit-binary.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commit-merge.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commit-modify-split.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commit-modify.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commit-rename.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commit-root.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commit-trojan.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commits-page2.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commits-path.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commits-root.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/commits-small.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/empty.html +0 -4
| @@ -51,10 +51,6 @@ | |||
| 51 | 51 | <span class="search-label">Go to…</span> | |
| 52 | 52 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 53 | 53 | </button> | |
| 54 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 55 | - | <span class="ico i-download" aria-hidden="true"> | |
| 56 | - | </span> | |
| 57 | - | </button> | |
| 58 | 54 | </div> | |
| 59 | 55 | </div> | |
| 60 | 56 | </header> | |
| @@ -51,10 +51,6 @@ | |||||
| 51 | <span class="search-label">Go to…</span> | 51 | <span class="search-label">Go to…</span> | ||
| 52 | <kbd aria-hidden="true">ctrl k</kbd> | 52 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 53 | </button> | 53 | </button> | ||
| 54 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 55 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 56 | - | </span> | |||
| 57 | - | </button> | |||
| 58 | </div> | 54 | </div> | ||
| 59 | </div> | 55 | </div> | ||
| 60 | </header> | 56 | </header> | ||
M internal/app/testdata/web/home.html +0 -4
| @@ -44,10 +44,6 @@ | |||
| 44 | 44 | <span class="search-label">Go to…</span> | |
| 45 | 45 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 46 | 46 | </button> | |
| 47 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 48 | - | <span class="ico i-download" aria-hidden="true"> | |
| 49 | - | </span> | |
| 50 | - | </button> | |
| 51 | 47 | </div> | |
| 52 | 48 | </div> | |
| 53 | 49 | </header> | |
| @@ -44,10 +44,6 @@ | |||||
| 44 | <span class="search-label">Go to…</span> | 44 | <span class="search-label">Go to…</span> | ||
| 45 | <kbd aria-hidden="true">ctrl k</kbd> | 45 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 46 | </button> | 46 | </button> | ||
| 47 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 48 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 49 | - | </span> | |||
| 50 | - | </button> | |||
| 51 | </div> | 47 | </div> | ||
| 52 | </div> | 48 | </div> | ||
| 53 | </header> | 49 | </header> | ||
M internal/app/testdata/web/not-found-page.html +0 -4
| @@ -51,10 +51,6 @@ | |||
| 51 | 51 | <span class="search-label">Go to…</span> | |
| 52 | 52 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 53 | 53 | </button> | |
| 54 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 55 | - | <span class="ico i-download" aria-hidden="true"> | |
| 56 | - | </span> | |
| 57 | - | </button> | |
| 58 | 54 | </div> | |
| 59 | 55 | </div> | |
| 60 | 56 | </header> | |
| @@ -51,10 +51,6 @@ | |||||
| 51 | <span class="search-label">Go to…</span> | 51 | <span class="search-label">Go to…</span> | ||
| 52 | <kbd aria-hidden="true">ctrl k</kbd> | 52 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 53 | </button> | 53 | </button> | ||
| 54 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 55 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 56 | - | </span> | |||
| 57 | - | </button> | |||
| 58 | </div> | 54 | </div> | ||
| 59 | </div> | 55 | </div> | ||
| 60 | </header> | 56 | </header> | ||
M internal/app/testdata/web/not-found.html +0 -4
| @@ -51,10 +51,6 @@ | |||
| 51 | 51 | <span class="search-label">Go to…</span> | |
| 52 | 52 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 53 | 53 | </button> | |
| 54 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 55 | - | <span class="ico i-download" aria-hidden="true"> | |
| 56 | - | </span> | |
| 57 | - | </button> | |
| 58 | 54 | </div> | |
| 59 | 55 | </div> | |
| 60 | 56 | </header> | |
| @@ -51,10 +51,6 @@ | |||||
| 51 | <span class="search-label">Go to…</span> | 51 | <span class="search-label">Go to…</span> | ||
| 52 | <kbd aria-hidden="true">ctrl k</kbd> | 52 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 53 | </button> | 53 | </button> | ||
| 54 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 55 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 56 | - | </span> | |||
| 57 | - | </button> | |||
| 58 | </div> | 54 | </div> | ||
| 59 | </div> | 55 | </div> | ||
| 60 | </header> | 56 | </header> | ||
M internal/app/testdata/web/refs.html +0 -4
| @@ -60,10 +60,6 @@ | |||
| 60 | 60 | <span class="search-label">Go to…</span> | |
| 61 | 61 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 62 | 62 | </button> | |
| 63 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 64 | - | <span class="ico i-download" aria-hidden="true"> | |
| 65 | - | </span> | |
| 66 | - | </button> | |
| 67 | 63 | </div> | |
| 68 | 64 | </div> | |
| 69 | 65 | <nav class="tabs" aria-label="Sections"> | |
| @@ -60,10 +60,6 @@ | |||||
| 60 | <span class="search-label">Go to…</span> | 60 | <span class="search-label">Go to…</span> | ||
| 61 | <kbd aria-hidden="true">ctrl k</kbd> | 61 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 62 | </button> | 62 | </button> | ||
| 63 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 64 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 65 | - | </span> | |||
| 66 | - | </button> | |||
| 67 | </div> | 63 | </div> | ||
| 68 | </div> | 64 | </div> | ||
| 69 | <nav class="tabs" aria-label="Sections"> | 65 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/summary.html +1 -9
| @@ -57,10 +57,6 @@ | |||
| 57 | 57 | <span class="search-label">Go to…</span> | |
| 58 | 58 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 59 | 59 | </button> | |
| 60 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 61 | - | <span class="ico i-download" aria-hidden="true"> | |
| 62 | - | </span> | |
| 63 | - | </button> | |
| 64 | 60 | </div> | |
| 65 | 61 | </div> | |
| 66 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -153,11 +149,7 @@ | |||
| 153 | 149 | </dd> | |
| 154 | 150 | <dt>latest tag</dt> | |
| 155 | 151 | <dd class="latest-tag"> | |
| 156 | - | <a class="tag-name" href="/demo/tree/v0.2.0" title="add main.go"> | |
| 157 | - | <span class="ico i-tag" aria-hidden="true"> | |
| 158 | - | </span>v0.2.0</a> | |
| 159 | - | <time class="dim" datetime="STAMP" title="STAMP">AGO</time> | |
| 160 | - | <a class="all-tags" href="/demo/refs">2 tags</a> | |
| 152 | + | <a class="tag-name" href="/demo/tree/v0.2.0" title="add main.go">v0.2.0</a> · <time datetime="STAMP" title="STAMP">AGO</time> · <a class="all-tags" href="/demo/refs">2 tags</a> | |
| 161 | 153 | </dd> | |
| 162 | 154 | </dl> | |
| 163 | 155 | </section> | |
| @@ -57,10 +57,6 @@ | |||||
| 57 | <span class="search-label">Go to…</span> | 57 | <span class="search-label">Go to…</span> | ||
| 58 | <kbd aria-hidden="true">ctrl k</kbd> | 58 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 59 | </button> | 59 | </button> | ||
| 60 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 61 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 62 | - | </span> | |||
| 63 | - | </button> | |||
| 64 | </div> | 60 | </div> | ||
| 65 | </div> | 61 | </div> | ||
| 66 | <nav class="tabs" aria-label="Sections"> | 62 | <nav class="tabs" aria-label="Sections"> | ||
| @@ -153,11 +149,7 @@ | |||||
| 153 | </dd> | 149 | </dd> | ||
| 154 | <dt>latest tag</dt> | 150 | <dt>latest tag</dt> | ||
| 155 | <dd class="latest-tag"> | 151 | <dd class="latest-tag"> | ||
| 156 | - | <a class="tag-name" href="/demo/tree/v0.2.0" title="add main.go"> | 152 | + | <a class="tag-name" href="/demo/tree/v0.2.0" title="add main.go">v0.2.0</a> · <time datetime="STAMP" title="STAMP">AGO</time> · <a class="all-tags" href="/demo/refs">2 tags</a> |
| 157 | - | <span class="ico i-tag" aria-hidden="true"> | |||
| 158 | - | </span>v0.2.0</a> | |||
| 159 | - | <time class="dim" datetime="STAMP" title="STAMP">AGO</time> | |||
| 160 | - | <a class="all-tags" href="/demo/refs">2 tags</a> | |||
| 161 | </dd> | 153 | </dd> | ||
| 162 | </dl> | 154 | </dl> | ||
| 163 | </section> | 155 | </section> | ||
M internal/app/testdata/web/tree-nested.html +0 -4
| @@ -69,10 +69,6 @@ | |||
| 69 | 69 | <span class="search-label">Go to…</span> | |
| 70 | 70 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 71 | 71 | </button> | |
| 72 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 73 | - | <span class="ico i-download" aria-hidden="true"> | |
| 74 | - | </span> | |
| 75 | - | </button> | |
| 76 | 72 | </div> | |
| 77 | 73 | </div> | |
| 78 | 74 | <nav class="tabs" aria-label="Sections"> | |
| @@ -69,10 +69,6 @@ | |||||
| 69 | <span class="search-label">Go to…</span> | 69 | <span class="search-label">Go to…</span> | ||
| 70 | <kbd aria-hidden="true">ctrl k</kbd> | 70 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 71 | </button> | 71 | </button> | ||
| 72 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 73 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 74 | - | </span> | |||
| 75 | - | </button> | |||
| 76 | </div> | 72 | </div> | ||
| 77 | </div> | 73 | </div> | ||
| 78 | <nav class="tabs" aria-label="Sections"> | 74 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/tree-root.html +0 -4
| @@ -63,10 +63,6 @@ | |||
| 63 | 63 | <span class="search-label">Go to…</span> | |
| 64 | 64 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 65 | 65 | </button> | |
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |
| 68 | - | </span> | |
| 69 | - | </button> | |
| 70 | 66 | </div> | |
| 71 | 67 | </div> | |
| 72 | 68 | <nav class="tabs" aria-label="Sections"> | |
| @@ -63,10 +63,6 @@ | |||||
| 63 | <span class="search-label">Go to…</span> | 63 | <span class="search-label">Go to…</span> | ||
| 64 | <kbd aria-hidden="true">ctrl k</kbd> | 64 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 65 | </button> | 65 | </button> | ||
| 66 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 67 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 68 | - | </span> | |||
| 69 | - | </button> | |||
| 70 | </div> | 66 | </div> | ||
| 71 | </div> | 67 | </div> | ||
| 72 | <nav class="tabs" aria-label="Sections"> | 68 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/tree-slash-ref.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/testdata/web/tree-subdir.html +0 -4
| @@ -66,10 +66,6 @@ | |||
| 66 | 66 | <span class="search-label">Go to…</span> | |
| 67 | 67 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 68 | 68 | </button> | |
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |
| 71 | - | </span> | |
| 72 | - | </button> | |
| 73 | 69 | </div> | |
| 74 | 70 | </div> | |
| 75 | 71 | <nav class="tabs" aria-label="Sections"> | |
| @@ -66,10 +66,6 @@ | |||||
| 66 | <span class="search-label">Go to…</span> | 66 | <span class="search-label">Go to…</span> | ||
| 67 | <kbd aria-hidden="true">ctrl k</kbd> | 67 | <kbd aria-hidden="true">ctrl k</kbd> | ||
| 68 | </button> | 68 | </button> | ||
| 69 | - | <button type="button" class="btn icon" data-install hidden aria-label="Install app" title="install ermite as an app"> | |||
| 70 | - | <span class="ico i-download" aria-hidden="true"> | |||
| 71 | - | </span> | |||
| 72 | - | </button> | |||
| 73 | </div> | 69 | </div> | ||
| 74 | </div> | 70 | </div> | ||
| 75 | <nav class="tabs" aria-label="Sections"> | 71 | <nav class="tabs" aria-label="Sections"> | ||
M internal/app/web_mr_test.go +9 -3
| @@ -98,7 +98,7 @@ func TestWebMergeRequestPagesRender(t *testing.T) { | |||
| 98 | 98 | // Open one from the compare page. | |
| 99 | 99 | res := b.form("/proj/merge-requests", token, url.Values{ | |
| 100 | 100 | "source": {"feat/search"}, "target": {"main"}, "title": {"Add <b>search</b>"}, | |
| 101 | - | "description": {"Finds things.\r\nSee https://example.org/spec) and javascript:alert(1)"}, | |
| 101 | + | "description": {"## Summary\r\n\r\nFinds **things** with `Find()`.\r\n\r\n- See https://example.org/spec) and javascript:alert(1)\r\n- [x](javascript:alert(1)) <script>alert(1)</script>"}, | |
| 102 | 102 | }) | |
| 103 | 103 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | |
| 104 | 104 | assert.Equal(t, "/proj/merge-requests/1", res.hdr.Get("Location")) | |
| @@ -137,9 +137,15 @@ func TestWebMergeRequestPagesRender(t *testing.T) { | |||
| 137 | 137 | assert.Contains(t, mr.body, "package search</td>") | |
| 138 | 138 | assert.Contains(t, mr.body, "no checks") | |
| 139 | 139 | assert.Contains(t, mr.body, "fast-forward, squash") | |
| 140 | - | assert.Contains(t, mr.body, `<a href="https://example.org/spec" rel="noopener nofollow ugc">https://example.org/spec</a>)`, "the address is linked, not its closing bracket") | |
| 140 | + | // The description is Markdown, rendered as a README: no raw HTML, no | |
| 141 | + | // script URL; its heading ids keep apart from the ids of the page. | |
| 142 | + | assert.Contains(t, mr.body, `<article class="mr-desc prose" aria-label="description" hx-ignore>`) | |
| 143 | + | assert.Contains(t, mr.body, `<h2 id="mr-desc-summary">Summary</h2>`) | |
| 144 | + | assert.Contains(t, mr.body, `<strong>things</strong>`) | |
| 145 | + | assert.Contains(t, mr.body, `<code>Find()</code>`) | |
| 146 | + | assert.Contains(t, mr.body, `<a href="https://example.org/spec"`, "a bare address is linked") | |
| 141 | 147 | assert.NotContains(t, mr.body, `href="javascript:`) | |
| 142 | - | assert.Contains(t, mr.body, "Finds things.\nSee ", "line breaks are kept") | |
| 148 | + | assert.NotContains(t, mr.body, `<script>alert(1)</script>`) | |
| 143 | 149 | assert.NotContains(t, mr.body, "<form", "an anonymous visitor gets no form") | |
| 144 | 150 | assert.NotContains(t, mr.body, "ermite mr merge", "nor the merge line") | |
| 145 | 151 | assert.NotContains(t, mr.body, `id="merge-menu"`) | |
| @@ -98,7 +98,7 @@ func TestWebMergeRequestPagesRender(t *testing.T) { | |||||
| 98 | // Open one from the compare page. | 98 | // Open one from the compare page. | ||
| 99 | res := b.form("/proj/merge-requests", token, url.Values{ | 99 | res := b.form("/proj/merge-requests", token, url.Values{ | ||
| 100 | "source": {"feat/search"}, "target": {"main"}, "title": {"Add <b>search</b>"}, | 100 | "source": {"feat/search"}, "target": {"main"}, "title": {"Add <b>search</b>"}, | ||
| 101 | - | "description": {"Finds things.\r\nSee https://example.org/spec) and javascript:alert(1)"}, | 101 | + | "description": {"## Summary\r\n\r\nFinds **things** with `Find()`.\r\n\r\n- See https://example.org/spec) and javascript:alert(1)\r\n- [x](javascript:alert(1)) <script>alert(1)</script>"}, |
| 102 | }) | 102 | }) | ||
| 103 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | 103 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | ||
| 104 | assert.Equal(t, "/proj/merge-requests/1", res.hdr.Get("Location")) | 104 | assert.Equal(t, "/proj/merge-requests/1", res.hdr.Get("Location")) | ||
| @@ -137,9 +137,15 @@ func TestWebMergeRequestPagesRender(t *testing.T) { | |||||
| 137 | assert.Contains(t, mr.body, "package search</td>") | 137 | assert.Contains(t, mr.body, "package search</td>") | ||
| 138 | assert.Contains(t, mr.body, "no checks") | 138 | assert.Contains(t, mr.body, "no checks") | ||
| 139 | assert.Contains(t, mr.body, "fast-forward, squash") | 139 | assert.Contains(t, mr.body, "fast-forward, squash") | ||
| 140 | - | assert.Contains(t, mr.body, `<a href="https://example.org/spec" rel="noopener nofollow ugc">https://example.org/spec</a>)`, "the address is linked, not its closing bracket") | 140 | + | // The description is Markdown, rendered as a README: no raw HTML, no |
| 141 | + | // script URL; its heading ids keep apart from the ids of the page. | |||
| 142 | + | assert.Contains(t, mr.body, `<article class="mr-desc prose" aria-label="description" hx-ignore>`) | |||
| 143 | + | assert.Contains(t, mr.body, `<h2 id="mr-desc-summary">Summary</h2>`) | |||
| 144 | + | assert.Contains(t, mr.body, `<strong>things</strong>`) | |||
| 145 | + | assert.Contains(t, mr.body, `<code>Find()</code>`) | |||
| 146 | + | assert.Contains(t, mr.body, `<a href="https://example.org/spec"`, "a bare address is linked") | |||
| 141 | assert.NotContains(t, mr.body, `href="javascript:`) | 147 | assert.NotContains(t, mr.body, `href="javascript:`) | ||
| 142 | - | assert.Contains(t, mr.body, "Finds things.\nSee ", "line breaks are kept") | 148 | + | assert.NotContains(t, mr.body, `<script>alert(1)</script>`) |
| 143 | assert.NotContains(t, mr.body, "<form", "an anonymous visitor gets no form") | 149 | assert.NotContains(t, mr.body, "<form", "an anonymous visitor gets no form") | ||
| 144 | assert.NotContains(t, mr.body, "ermite mr merge", "nor the merge line") | 150 | assert.NotContains(t, mr.body, "ermite mr merge", "nor the merge line") | ||
| 145 | assert.NotContains(t, mr.body, `id="merge-menu"`) | 151 | assert.NotContains(t, mr.body, `id="merge-menu"`) | ||