feat(web): footer theme switch for auto, light and dark modes
- commit
- bb7efc81285aee2b61f392621f5d6de458adc7ef
- author
- brice · · 2026-10-22 09:45 UTC
- parent
- 7c1886f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
42 files · +374 -8
M internal/adapters/web/ASSETS.md +2 -2
| @@ -8,7 +8,7 @@ content-hashed names. Nothing is fetched at runtime. | |||
| 8 | 8 | | `static/js/htmx.min.js` | htmx 4.0.0 (BSD Zero Clause, `static/js/htmx-LICENSE`) | `mockups/assets/htmx/htmx.min.js` of the design | `e484d9171a9db30a39c8f16e3d709d4137f3211c659f8e6125816635033d593f` | | |
| 9 | 9 | | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` | | |
| 10 | 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | |
| 11 | - | | `static/js/store.js` | the localStorage client of the scripts: preferences such as the home layout and the single-key shortcuts (`window.ermiteStore`, tested in Node by `jstest/store.test.mjs`) | written here | | | |
| 11 | + | | `static/js/store.js` | the localStorage client of the scripts: preferences such as the home layout and the single-key shortcuts (`window.ermiteStore`, tested in Node by `jstest/store.test.mjs`); applies the wide mode and the theme (`window.ermiteTheme`: auto, light or dark, key `theme`; tested by `jstest/theme.test.mjs`) before the first paint | written here | | | |
| 12 | 12 | | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | | | |
| 13 | 13 | | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | | | |
| 14 | 14 | | `static/js/files.js` | the Files view: a tree whose directories open in place and walk by keyboard (`window.ermiteTree`, tested in Node by `jstest/tree.test.mjs`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | | | |
| @@ -19,7 +19,7 @@ content-hashed names. Nothing is fetched at runtime. | |||
| 19 | 19 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | |
| 20 | 20 | | `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` | | |
| 21 | 21 | | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` | | |
| 22 | - | | `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`, `cards`, `split`, `compact` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`, `rows-2`, `columns-2`, `list`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | | |
| 22 | + | | `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`, `cards`, `split`, `compact`, `sun-moon`, `sun`, `moon` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`, `rows-2`, `columns-2`, `list`, `sun-moon`, `sun`, `moon`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | | |
| 23 | 23 | | `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 | | | |
| 24 | 24 | | `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 | | | |
| 25 | 25 | | `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 | | | |
| @@ -8,7 +8,7 @@ content-hashed names. Nothing is fetched at runtime. | |||||
| 8 | | `static/js/htmx.min.js` | htmx 4.0.0 (BSD Zero Clause, `static/js/htmx-LICENSE`) | `mockups/assets/htmx/htmx.min.js` of the design | `e484d9171a9db30a39c8f16e3d709d4137f3211c659f8e6125816635033d593f` | | 8 | | `static/js/htmx.min.js` | htmx 4.0.0 (BSD Zero Clause, `static/js/htmx-LICENSE`) | `mockups/assets/htmx/htmx.min.js` of the design | `e484d9171a9db30a39c8f16e3d709d4137f3211c659f8e6125816635033d593f` | | ||
| 9 | | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` | | 9 | | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` | | ||
| 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | ||
| 11 | - | | `static/js/store.js` | the localStorage client of the scripts: preferences such as the home layout and the single-key shortcuts (`window.ermiteStore`, tested in Node by `jstest/store.test.mjs`) | written here | | | 11 | + | | `static/js/store.js` | the localStorage client of the scripts: preferences such as the home layout and the single-key shortcuts (`window.ermiteStore`, tested in Node by `jstest/store.test.mjs`); applies the wide mode and the theme (`window.ermiteTheme`: auto, light or dark, key `theme`; tested by `jstest/theme.test.mjs`) before the first paint | written here | | |
| 12 | | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | | | 12 | | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | | | ||
| 13 | | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | | | 13 | | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | | | ||
| 14 | | `static/js/files.js` | the Files view: a tree whose directories open in place and walk by keyboard (`window.ermiteTree`, tested in Node by `jstest/tree.test.mjs`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | | | 14 | | `static/js/files.js` | the Files view: a tree whose directories open in place and walk by keyboard (`window.ermiteTree`, tested in Node by `jstest/tree.test.mjs`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | | | ||
| @@ -19,7 +19,7 @@ content-hashed names. Nothing is fetched at runtime. | |||||
| 19 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | 19 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | ||
| 20 | | `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` | | 20 | | `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` | | ||
| 21 | | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` | | 21 | | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` | | ||
| 22 | - | | `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`, `cards`, `split`, `compact` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`, `rows-2`, `columns-2`, `list`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | | 22 | + | | `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`, `cards`, `split`, `compact`, `sun-moon`, `sun`, `moon` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`, `rows-2`, `columns-2`, `list`, `sun-moon`, `sun`, `moon`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | |
| 23 | | `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 | | | 23 | | `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 | | | ||
| 24 | | `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 | | | 24 | | `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 | | | ||
| 25 | | `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 | | | 25 | | `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 | | | ||
A internal/adapters/web/jstest/theme.test.mjs +139 -0
| @@ -0,0 +1,139 @@ | |||
| 1 | + | // Tests of the theme: window.ermiteTheme of static/js/store.js (applied before | |
| 2 | + | // the first paint) and the footer switch of static/js/ermite.js | |
| 3 | + | // (window.ermiteThemeSwitch), on the small DOM of dom.mjs. Only the | |
| 4 | + | // ermiteThemeSwitch block of ermite.js is evaluated: the rest needs a page. | |
| 5 | + | // Run with | |
| 6 | + | // node --test 'internal/adapters/web/jstest/*.test.mjs' | |
| 7 | + | import assert from 'node:assert/strict'; | |
| 8 | + | import { readFileSync } from 'node:fs'; | |
| 9 | + | import { beforeEach, test } from 'node:test'; | |
| 10 | + | import { el, installDom } from './dom.mjs'; | |
| 11 | + | ||
| 12 | + | const read = (f) => readFileSync(new URL('../static/js/' + f, import.meta.url), 'utf8'); | |
| 13 | + | const ermite = read('ermite.js'); | |
| 14 | + | const start = ermite.indexOf(' window.ermiteThemeSwitch = '); | |
| 15 | + | const block = ermite.slice(start, ermite.indexOf(' })();', start) + ' })();'.length); | |
| 16 | + | const storeSrc = read('store.js'); | |
| 17 | + | ||
| 18 | + | const doc = installDom(); | |
| 19 | + | let listeners; | |
| 20 | + | let storage; | |
| 21 | + | let root; | |
| 22 | + | let scheme; | |
| 23 | + | let colors; | |
| 24 | + | let button; | |
| 25 | + | let icon; | |
| 26 | + | ||
| 27 | + | // A page as shell.templ renders it: the metas and the footer button in auto. | |
| 28 | + | // Then store.js runs (in <head>) and, unless withSwitch is false, the block of | |
| 29 | + | // ermite.js (deferred). | |
| 30 | + | function load({ stored, withSwitch = true } = {}) { | |
| 31 | + | listeners = {}; | |
| 32 | + | storage = new Map(stored ? Object.entries(stored) : []); | |
| 33 | + | globalThis.localStorage = { | |
| 34 | + | getItem: (k) => (storage.has(k) ? storage.get(k) : null), | |
| 35 | + | setItem: (k, v) => { storage.set(k, String(v)); }, | |
| 36 | + | removeItem: (k) => { storage.delete(k); }, | |
| 37 | + | }; | |
| 38 | + | globalThis.addEventListener = (type, f) => (listeners[type] ||= []).push(f); | |
| 39 | + | globalThis.removeEventListener = (type, f) => { listeners[type] = (listeners[type] || []).filter((g) => g !== f); }; | |
| 40 | + | root = el('html'); | |
| 41 | + | doc.documentElement = root; | |
| 42 | + | scheme = el('meta', { attrs: { name: 'color-scheme', content: 'light dark' } }); | |
| 43 | + | colors = [ | |
| 44 | + | el('meta', { attrs: { name: 'theme-color', media: '(prefers-color-scheme: light)', content: '#ffffff' } }), | |
| 45 | + | el('meta', { attrs: { name: 'theme-color', media: '(prefers-color-scheme: dark)', content: '#0c0c0c' } }), | |
| 46 | + | ]; | |
| 47 | + | icon = el('span', { cls: 'ico i-sun-moon', attrs: { 'aria-hidden': 'true' } }); | |
| 48 | + | button = el('button', { cls: 'foot-theme', attrs: { type: 'button', 'data-js': '', 'data-theme-switch': '' } }, icon); | |
| 49 | + | doc.body.replaceChildren(scheme, ...colors, el('footer', { cls: 'foot' }, button)); | |
| 50 | + | new Function(storeSrc)(); | |
| 51 | + | if (withSwitch) new Function(block)(); | |
| 52 | + | return window.ermiteTheme; | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | const state = () => ({ | |
| 56 | + | attr: root.getAttribute('data-theme'), | |
| 57 | + | scheme: scheme.getAttribute('content'), | |
| 58 | + | colors: colors.map((m) => m.getAttribute('content')), | |
| 59 | + | }); | |
| 60 | + | const click = (target) => listeners.click.forEach((f) => f({ target })); | |
| 61 | + | const storageEvent = (newValue) => listeners.storage.forEach((f) => f({ key: 'theme', newValue, storageArea: localStorage })); | |
| 62 | + | ||
| 63 | + | beforeEach(() => { delete globalThis.localStorage; }); | |
| 64 | + | ||
| 65 | + | test('nothing stored: auto, the page is left to the system', () => { | |
| 66 | + | const T = load({ withSwitch: false }); | |
| 67 | + | assert.equal(T.get(), 'auto'); | |
| 68 | + | assert.deepEqual(state(), { attr: null, scheme: 'light dark', colors: ['#ffffff', '#0c0c0c'] }); | |
| 69 | + | }); | |
| 70 | + | ||
| 71 | + | test('store.js applies a stored theme before ermite.js runs', () => { | |
| 72 | + | load({ stored: { theme: 'dark' }, withSwitch: false }); | |
| 73 | + | assert.deepEqual(state(), { attr: 'dark', scheme: 'dark', colors: ['#0c0c0c', '#0c0c0c'] }); | |
| 74 | + | load({ stored: { theme: 'light' }, withSwitch: false }); | |
| 75 | + | assert.deepEqual(state(), { attr: 'light', scheme: 'light', colors: ['#ffffff', '#ffffff'] }); | |
| 76 | + | }); | |
| 77 | + | ||
| 78 | + | test('a stored value that is not a theme falls back to auto', () => { | |
| 79 | + | const T = load({ stored: { theme: 'sepia' } }); | |
| 80 | + | assert.equal(T.get(), 'auto'); | |
| 81 | + | assert.equal(root.getAttribute('data-theme'), null); | |
| 82 | + | }); | |
| 83 | + | ||
| 84 | + | test('next cycles auto, light, dark', () => { | |
| 85 | + | const T = load({ withSwitch: false }); | |
| 86 | + | assert.deepEqual(T.values.map(T.next), ['light', 'dark', 'auto']); | |
| 87 | + | }); | |
| 88 | + | ||
| 89 | + | test('a click on the button (or its icon) cycles, stores and shows the theme', () => { | |
| 90 | + | load(); | |
| 91 | + | assert.equal(button.getAttribute('aria-label'), 'theme: auto (click for light)'); | |
| 92 | + | click(icon); | |
| 93 | + | assert.equal(storage.get('theme'), 'light'); | |
| 94 | + | assert.deepEqual(state(), { attr: 'light', scheme: 'light', colors: ['#ffffff', '#ffffff'] }); | |
| 95 | + | assert.equal(icon.className, 'ico i-sun'); | |
| 96 | + | assert.equal(button.getAttribute('aria-label'), 'theme: light (click for dark)'); | |
| 97 | + | assert.equal(button.getAttribute('title'), 'theme: light (click for dark)'); | |
| 98 | + | click(button); | |
| 99 | + | assert.equal(storage.get('theme'), 'dark'); | |
| 100 | + | assert.deepEqual(state(), { attr: 'dark', scheme: 'dark', colors: ['#0c0c0c', '#0c0c0c'] }); | |
| 101 | + | assert.equal(icon.className, 'ico i-moon'); | |
| 102 | + | click(button); | |
| 103 | + | assert.equal(storage.get('theme'), 'auto'); | |
| 104 | + | assert.deepEqual(state(), { attr: null, scheme: 'light dark', colors: ['#ffffff', '#0c0c0c'] }); | |
| 105 | + | assert.equal(icon.className, 'ico i-sun-moon'); | |
| 106 | + | assert.equal(button.getAttribute('aria-label'), 'theme: auto (click for light)'); | |
| 107 | + | }); | |
| 108 | + | ||
| 109 | + | test('other clicks do nothing', () => { | |
| 110 | + | const T = load(); | |
| 111 | + | click(doc.body); | |
| 112 | + | click({ closest: undefined }); | |
| 113 | + | assert.equal(T.get(), 'auto'); | |
| 114 | + | }); | |
| 115 | + | ||
| 116 | + | test("another tab's change applies live", () => { | |
| 117 | + | const T = load(); | |
| 118 | + | storage.set('theme', 'dark'); | |
| 119 | + | storageEvent('dark'); | |
| 120 | + | assert.equal(T.get(), 'dark'); | |
| 121 | + | assert.equal(root.getAttribute('data-theme'), 'dark'); | |
| 122 | + | assert.equal(icon.className, 'ico i-moon'); | |
| 123 | + | storage.delete('theme'); | |
| 124 | + | storageEvent(null); | |
| 125 | + | assert.equal(root.getAttribute('data-theme'), null, 'removed: back to auto'); | |
| 126 | + | assert.equal(button.getAttribute('aria-label'), 'theme: auto (click for light)'); | |
| 127 | + | }); | |
| 128 | + | ||
| 129 | + | test('without usable storage the choice holds for the page', () => { | |
| 130 | + | load(); | |
| 131 | + | globalThis.localStorage = { | |
| 132 | + | getItem: () => { throw new Error('denied'); }, | |
| 133 | + | setItem: () => { throw new Error('quota'); }, | |
| 134 | + | removeItem: () => { throw new Error('denied'); }, | |
| 135 | + | }; | |
| 136 | + | click(button); | |
| 137 | + | assert.equal(window.ermiteTheme.get(), 'light'); | |
| 138 | + | assert.equal(root.getAttribute('data-theme'), 'light'); | |
| 139 | + | }); | |
| @@ -0,0 +1,139 @@ | |||||
| 1 | + | // Tests of the theme: window.ermiteTheme of static/js/store.js (applied before | |||
| 2 | + | // the first paint) and the footer switch of static/js/ermite.js | |||
| 3 | + | // (window.ermiteThemeSwitch), on the small DOM of dom.mjs. Only the | |||
| 4 | + | // ermiteThemeSwitch block of ermite.js is evaluated: the rest needs a page. | |||
| 5 | + | // Run with | |||
| 6 | + | // node --test 'internal/adapters/web/jstest/*.test.mjs' | |||
| 7 | + | import assert from 'node:assert/strict'; | |||
| 8 | + | import { readFileSync } from 'node:fs'; | |||
| 9 | + | import { beforeEach, test } from 'node:test'; | |||
| 10 | + | import { el, installDom } from './dom.mjs'; | |||
| 11 | + | ||||
| 12 | + | const read = (f) => readFileSync(new URL('../static/js/' + f, import.meta.url), 'utf8'); | |||
| 13 | + | const ermite = read('ermite.js'); | |||
| 14 | + | const start = ermite.indexOf(' window.ermiteThemeSwitch = '); | |||
| 15 | + | const block = ermite.slice(start, ermite.indexOf(' })();', start) + ' })();'.length); | |||
| 16 | + | const storeSrc = read('store.js'); | |||
| 17 | + | ||||
| 18 | + | const doc = installDom(); | |||
| 19 | + | let listeners; | |||
| 20 | + | let storage; | |||
| 21 | + | let root; | |||
| 22 | + | let scheme; | |||
| 23 | + | let colors; | |||
| 24 | + | let button; | |||
| 25 | + | let icon; | |||
| 26 | + | ||||
| 27 | + | // A page as shell.templ renders it: the metas and the footer button in auto. | |||
| 28 | + | // Then store.js runs (in <head>) and, unless withSwitch is false, the block of | |||
| 29 | + | // ermite.js (deferred). | |||
| 30 | + | function load({ stored, withSwitch = true } = {}) { | |||
| 31 | + | listeners = {}; | |||
| 32 | + | storage = new Map(stored ? Object.entries(stored) : []); | |||
| 33 | + | globalThis.localStorage = { | |||
| 34 | + | getItem: (k) => (storage.has(k) ? storage.get(k) : null), | |||
| 35 | + | setItem: (k, v) => { storage.set(k, String(v)); }, | |||
| 36 | + | removeItem: (k) => { storage.delete(k); }, | |||
| 37 | + | }; | |||
| 38 | + | globalThis.addEventListener = (type, f) => (listeners[type] ||= []).push(f); | |||
| 39 | + | globalThis.removeEventListener = (type, f) => { listeners[type] = (listeners[type] || []).filter((g) => g !== f); }; | |||
| 40 | + | root = el('html'); | |||
| 41 | + | doc.documentElement = root; | |||
| 42 | + | scheme = el('meta', { attrs: { name: 'color-scheme', content: 'light dark' } }); | |||
| 43 | + | colors = [ | |||
| 44 | + | el('meta', { attrs: { name: 'theme-color', media: '(prefers-color-scheme: light)', content: '#ffffff' } }), | |||
| 45 | + | el('meta', { attrs: { name: 'theme-color', media: '(prefers-color-scheme: dark)', content: '#0c0c0c' } }), | |||
| 46 | + | ]; | |||
| 47 | + | icon = el('span', { cls: 'ico i-sun-moon', attrs: { 'aria-hidden': 'true' } }); | |||
| 48 | + | button = el('button', { cls: 'foot-theme', attrs: { type: 'button', 'data-js': '', 'data-theme-switch': '' } }, icon); | |||
| 49 | + | doc.body.replaceChildren(scheme, ...colors, el('footer', { cls: 'foot' }, button)); | |||
| 50 | + | new Function(storeSrc)(); | |||
| 51 | + | if (withSwitch) new Function(block)(); | |||
| 52 | + | return window.ermiteTheme; | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | const state = () => ({ | |||
| 56 | + | attr: root.getAttribute('data-theme'), | |||
| 57 | + | scheme: scheme.getAttribute('content'), | |||
| 58 | + | colors: colors.map((m) => m.getAttribute('content')), | |||
| 59 | + | }); | |||
| 60 | + | const click = (target) => listeners.click.forEach((f) => f({ target })); | |||
| 61 | + | const storageEvent = (newValue) => listeners.storage.forEach((f) => f({ key: 'theme', newValue, storageArea: localStorage })); | |||
| 62 | + | ||||
| 63 | + | beforeEach(() => { delete globalThis.localStorage; }); | |||
| 64 | + | ||||
| 65 | + | test('nothing stored: auto, the page is left to the system', () => { | |||
| 66 | + | const T = load({ withSwitch: false }); | |||
| 67 | + | assert.equal(T.get(), 'auto'); | |||
| 68 | + | assert.deepEqual(state(), { attr: null, scheme: 'light dark', colors: ['#ffffff', '#0c0c0c'] }); | |||
| 69 | + | }); | |||
| 70 | + | ||||
| 71 | + | test('store.js applies a stored theme before ermite.js runs', () => { | |||
| 72 | + | load({ stored: { theme: 'dark' }, withSwitch: false }); | |||
| 73 | + | assert.deepEqual(state(), { attr: 'dark', scheme: 'dark', colors: ['#0c0c0c', '#0c0c0c'] }); | |||
| 74 | + | load({ stored: { theme: 'light' }, withSwitch: false }); | |||
| 75 | + | assert.deepEqual(state(), { attr: 'light', scheme: 'light', colors: ['#ffffff', '#ffffff'] }); | |||
| 76 | + | }); | |||
| 77 | + | ||||
| 78 | + | test('a stored value that is not a theme falls back to auto', () => { | |||
| 79 | + | const T = load({ stored: { theme: 'sepia' } }); | |||
| 80 | + | assert.equal(T.get(), 'auto'); | |||
| 81 | + | assert.equal(root.getAttribute('data-theme'), null); | |||
| 82 | + | }); | |||
| 83 | + | ||||
| 84 | + | test('next cycles auto, light, dark', () => { | |||
| 85 | + | const T = load({ withSwitch: false }); | |||
| 86 | + | assert.deepEqual(T.values.map(T.next), ['light', 'dark', 'auto']); | |||
| 87 | + | }); | |||
| 88 | + | ||||
| 89 | + | test('a click on the button (or its icon) cycles, stores and shows the theme', () => { | |||
| 90 | + | load(); | |||
| 91 | + | assert.equal(button.getAttribute('aria-label'), 'theme: auto (click for light)'); | |||
| 92 | + | click(icon); | |||
| 93 | + | assert.equal(storage.get('theme'), 'light'); | |||
| 94 | + | assert.deepEqual(state(), { attr: 'light', scheme: 'light', colors: ['#ffffff', '#ffffff'] }); | |||
| 95 | + | assert.equal(icon.className, 'ico i-sun'); | |||
| 96 | + | assert.equal(button.getAttribute('aria-label'), 'theme: light (click for dark)'); | |||
| 97 | + | assert.equal(button.getAttribute('title'), 'theme: light (click for dark)'); | |||
| 98 | + | click(button); | |||
| 99 | + | assert.equal(storage.get('theme'), 'dark'); | |||
| 100 | + | assert.deepEqual(state(), { attr: 'dark', scheme: 'dark', colors: ['#0c0c0c', '#0c0c0c'] }); | |||
| 101 | + | assert.equal(icon.className, 'ico i-moon'); | |||
| 102 | + | click(button); | |||
| 103 | + | assert.equal(storage.get('theme'), 'auto'); | |||
| 104 | + | assert.deepEqual(state(), { attr: null, scheme: 'light dark', colors: ['#ffffff', '#0c0c0c'] }); | |||
| 105 | + | assert.equal(icon.className, 'ico i-sun-moon'); | |||
| 106 | + | assert.equal(button.getAttribute('aria-label'), 'theme: auto (click for light)'); | |||
| 107 | + | }); | |||
| 108 | + | ||||
| 109 | + | test('other clicks do nothing', () => { | |||
| 110 | + | const T = load(); | |||
| 111 | + | click(doc.body); | |||
| 112 | + | click({ closest: undefined }); | |||
| 113 | + | assert.equal(T.get(), 'auto'); | |||
| 114 | + | }); | |||
| 115 | + | ||||
| 116 | + | test("another tab's change applies live", () => { | |||
| 117 | + | const T = load(); | |||
| 118 | + | storage.set('theme', 'dark'); | |||
| 119 | + | storageEvent('dark'); | |||
| 120 | + | assert.equal(T.get(), 'dark'); | |||
| 121 | + | assert.equal(root.getAttribute('data-theme'), 'dark'); | |||
| 122 | + | assert.equal(icon.className, 'ico i-moon'); | |||
| 123 | + | storage.delete('theme'); | |||
| 124 | + | storageEvent(null); | |||
| 125 | + | assert.equal(root.getAttribute('data-theme'), null, 'removed: back to auto'); | |||
| 126 | + | assert.equal(button.getAttribute('aria-label'), 'theme: auto (click for light)'); | |||
| 127 | + | }); | |||
| 128 | + | ||||
| 129 | + | test('without usable storage the choice holds for the page', () => { | |||
| 130 | + | load(); | |||
| 131 | + | globalThis.localStorage = { | |||
| 132 | + | getItem: () => { throw new Error('denied'); }, | |||
| 133 | + | setItem: () => { throw new Error('quota'); }, | |||
| 134 | + | removeItem: () => { throw new Error('denied'); }, | |||
| 135 | + | }; | |||
| 136 | + | click(button); | |||
| 137 | + | assert.equal(window.ermiteTheme.get(), 'light'); | |||
| 138 | + | assert.equal(root.getAttribute('data-theme'), 'light'); | |||
| 139 | + | }); | |||
M internal/adapters/web/shell.templ +4 -1
| @@ -192,12 +192,15 @@ templ Header(l Layout) { | |||
| 192 | 192 | } | |
| 193 | 193 | ||
| 194 | 194 | // Footer is the version and the link to the source of the running version, | |
| 195 | - | // which the AGPL §13 asks every user of the service to be offered. | |
| 195 | + | // which the AGPL §13 asks every user of the service to be offered, then the | |
| 196 | + | // keyboard shortcuts and the theme switch (auto, light, dark; ermite.js shows | |
| 197 | + | // the stored mode). | |
| 196 | 198 | templ Footer(l Layout) { | |
| 197 | 199 | <footer class="foot"> | |
| 198 | 200 | <span>ermite { l.Version }</span> | |
| 199 | 201 | <a href={ templ.SafeURL(l.SourceURL) } rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 200 | 202 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"><kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 203 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"><span class="ico i-sun-moon" aria-hidden="true"></span></button> | |
| 201 | 204 | </footer> | |
| 202 | 205 | } | |
| 203 | 206 | ||
| @@ -192,12 +192,15 @@ templ Header(l Layout) { | |||||
| 192 | } | 192 | } | ||
| 193 | 193 | ||||
| 194 | // Footer is the version and the link to the source of the running version, | 194 | // Footer is the version and the link to the source of the running version, | ||
| 195 | - | // which the AGPL §13 asks every user of the service to be offered. | 195 | + | // which the AGPL §13 asks every user of the service to be offered, then the |
| 196 | + | // keyboard shortcuts and the theme switch (auto, light, dark; ermite.js shows | |||
| 197 | + | // the stored mode). | |||
| 196 | templ Footer(l Layout) { | 198 | templ Footer(l Layout) { | ||
| 197 | <footer class="foot"> | 199 | <footer class="foot"> | ||
| 198 | <span>ermite { l.Version }</span> | 200 | <span>ermite { l.Version }</span> | ||
| 199 | <a href={ templ.SafeURL(l.SourceURL) } rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 201 | <a href={ templ.SafeURL(l.SourceURL) } rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 200 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"><kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 202 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"><kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 203 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"><span class="ico i-sun-moon" aria-hidden="true"></span></button> | |||
| 201 | </footer> | 204 | </footer> | ||
| 202 | } | 205 | } | ||
| 203 | 206 | ||||
M internal/adapters/web/shell_templ.go +6 -4
| @@ -842,7 +842,9 @@ func Header(l Layout) templ.Component { | |||
| 842 | 842 | } | |
| 843 | 843 | ||
| 844 | 844 | // Footer is the version and the link to the source of the running version, | |
| 845 | - | // which the AGPL §13 asks every user of the service to be offered. | |
| 845 | + | // which the AGPL §13 asks every user of the service to be offered, then the | |
| 846 | + | // keyboard shortcuts and the theme switch (auto, light, dark; ermite.js shows | |
| 847 | + | // the stored mode). | |
| 846 | 848 | func Footer(l Layout) templ.Component { | |
| 847 | 849 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 848 | 850 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| @@ -871,7 +873,7 @@ func Footer(l Layout) templ.Component { | |||
| 871 | 873 | var templ_7745c5c3_Var44 string | |
| 872 | 874 | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | |
| 873 | 875 | if templ_7745c5c3_Err != nil { | |
| 874 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 198, Col: 26} | |
| 876 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 200, Col: 26} | |
| 875 | 877 | } | |
| 876 | 878 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | |
| 877 | 879 | if templ_7745c5c3_Err != nil { | |
| @@ -884,13 +886,13 @@ func Footer(l Layout) templ.Component { | |||
| 884 | 886 | var templ_7745c5c3_Var45 templ.SafeURL | |
| 885 | 887 | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | |
| 886 | 888 | if templ_7745c5c3_Err != nil { | |
| 887 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 199, Col: 38} | |
| 889 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 201, Col: 38} | |
| 888 | 890 | } | |
| 889 | 891 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | |
| 890 | 892 | if templ_7745c5c3_Err != nil { | |
| 891 | 893 | return templ_7745c5c3_Err | |
| 892 | 894 | } | |
| 893 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>") | |
| 895 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button> <button type=\"button\" class=\"foot-theme\" data-js data-theme-switch aria-label=\"theme: auto (click for light)\" title=\"theme: auto (click for light)\"><span class=\"ico i-sun-moon\" aria-hidden=\"true\"></span></button></footer>") | |
| 894 | 896 | if templ_7745c5c3_Err != nil { | |
| 895 | 897 | return templ_7745c5c3_Err | |
| 896 | 898 | } | |
| @@ -842,7 +842,9 @@ func Header(l Layout) templ.Component { | |||||
| 842 | } | 842 | } | ||
| 843 | 843 | ||||
| 844 | // Footer is the version and the link to the source of the running version, | 844 | // Footer is the version and the link to the source of the running version, | ||
| 845 | - | // which the AGPL §13 asks every user of the service to be offered. | 845 | + | // which the AGPL §13 asks every user of the service to be offered, then the |
| 846 | + | // keyboard shortcuts and the theme switch (auto, light, dark; ermite.js shows | |||
| 847 | + | // the stored mode). | |||
| 846 | func Footer(l Layout) templ.Component { | 848 | func Footer(l Layout) templ.Component { | ||
| 847 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | 849 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | ||
| 848 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | 850 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | ||
| @@ -871,7 +873,7 @@ func Footer(l Layout) templ.Component { | |||||
| 871 | var templ_7745c5c3_Var44 string | 873 | var templ_7745c5c3_Var44 string | ||
| 872 | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | 874 | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | ||
| 873 | if templ_7745c5c3_Err != nil { | 875 | if templ_7745c5c3_Err != nil { | ||
| 874 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 198, Col: 26} | 876 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 200, Col: 26} |
| 875 | } | 877 | } | ||
| 876 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | 878 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | ||
| 877 | if templ_7745c5c3_Err != nil { | 879 | if templ_7745c5c3_Err != nil { | ||
| @@ -884,13 +886,13 @@ func Footer(l Layout) templ.Component { | |||||
| 884 | var templ_7745c5c3_Var45 templ.SafeURL | 886 | var templ_7745c5c3_Var45 templ.SafeURL | ||
| 885 | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | 887 | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | ||
| 886 | if templ_7745c5c3_Err != nil { | 888 | if templ_7745c5c3_Err != nil { | ||
| 887 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 199, Col: 38} | 889 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 201, Col: 38} |
| 888 | } | 890 | } | ||
| 889 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | 891 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | ||
| 890 | if templ_7745c5c3_Err != nil { | 892 | if templ_7745c5c3_Err != nil { | ||
| 891 | return templ_7745c5c3_Err | 893 | return templ_7745c5c3_Err | ||
| 892 | } | 894 | } | ||
| 893 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>") | 895 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button> <button type=\"button\" class=\"foot-theme\" data-js data-theme-switch aria-label=\"theme: auto (click for light)\" title=\"theme: auto (click for light)\"><span class=\"ico i-sun-moon\" aria-hidden=\"true\"></span></button></footer>") |
| 894 | if templ_7745c5c3_Err != nil { | 896 | if templ_7745c5c3_Err != nil { | ||
| 895 | return templ_7745c5c3_Err | 897 | return templ_7745c5c3_Err | ||
| 896 | } | 898 | } | ||
M internal/adapters/web/static/css/00-tokens.css +4 -0
| @@ -27,7 +27,11 @@ | |||
| 27 | 27 | cursor, links in prose, open folders. */ | |
| 28 | 28 | @layer tokens { | |
| 29 | 29 | :root { | |
| 30 | + | /* the system's scheme, unless the theme switch forces one | |
| 31 | + | (html[data-theme], set by store.js) */ | |
| 30 | 32 | color-scheme: light dark; | |
| 33 | + | &[data-theme="light"] { color-scheme: light; } | |
| 34 | + | &[data-theme="dark"] { color-scheme: dark; } | |
| 31 | 35 | ||
| 32 | 36 | /* surfaces, bottom to top */ | |
| 33 | 37 | --bg-canvas: light-dark(#f3f3f3, #050505); /* the page */ | |
| @@ -27,7 +27,11 @@ | |||||
| 27 | cursor, links in prose, open folders. */ | 27 | cursor, links in prose, open folders. */ | ||
| 28 | @layer tokens { | 28 | @layer tokens { | ||
| 29 | :root { | 29 | :root { | ||
| 30 | + | /* the system's scheme, unless the theme switch forces one | |||
| 31 | + | (html[data-theme], set by store.js) */ | |||
| 30 | color-scheme: light dark; | 32 | color-scheme: light dark; | ||
| 33 | + | &[data-theme="light"] { color-scheme: light; } | |||
| 34 | + | &[data-theme="dark"] { color-scheme: dark; } | |||
| 31 | 35 | ||||
| 32 | /* surfaces, bottom to top */ | 36 | /* surfaces, bottom to top */ | ||
| 33 | --bg-canvas: light-dark(#f3f3f3, #050505); /* the page */ | 37 | --bg-canvas: light-dark(#f3f3f3, #050505); /* the page */ | ||
M internal/adapters/web/static/css/20-shell.css +9 -1
| @@ -30,6 +30,9 @@ | |||
| 30 | 30 | .i-split { --i: url(icons/split.svg); } | |
| 31 | 31 | .i-compact { --i: url(icons/compact.svg); } | |
| 32 | 32 | .i-mailbox { --i: url(icons/mailbox.svg); } | |
| 33 | + | .i-sun-moon { --i: url(icons/sun-moon.svg); } | |
| 34 | + | .i-sun { --i: url(icons/sun.svg); } | |
| 35 | + | .i-moon { --i: url(icons/moon.svg); } | |
| 33 | 36 | ||
| 34 | 37 | .pill { | |
| 35 | 38 | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | |
| @@ -185,7 +188,7 @@ | |||
| 185 | 188 | & .l4, & .l5, & .l6 { padding-left: var(--s6); } | |
| 186 | 189 | } | |
| 187 | 190 | ||
| 188 | - | /* footer: version and source (AGPL §13) */ | |
| 191 | + | /* footer: version and source (AGPL §13), then shortcuts and theme */ | |
| 189 | 192 | .foot { | |
| 190 | 193 | display: flex; flex-wrap: wrap; gap: var(--s4); align-items: center; | |
| 191 | 194 | width: 100%; max-width: var(--page); margin: 0 auto; padding: var(--s4) var(--pad) var(--s5); | |
| @@ -194,6 +197,11 @@ | |||
| 194 | 197 | & a { color: var(--ink-dim); } | |
| 195 | 198 | & .foot-keys { margin-left: auto; color: var(--ink-dim); } | |
| 196 | 199 | & .foot-keys:hover { color: var(--ink-hi); } | |
| 200 | + | & .foot-theme { | |
| 201 | + | display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; | |
| 202 | + | margin: -6px 0; color: var(--ink-dim); border-radius: var(--r-s); | |
| 203 | + | } | |
| 204 | + | & .foot-theme:hover { color: var(--ink-hi); background: var(--bg-raised); } | |
| 197 | 205 | } | |
| 198 | 206 | ||
| 199 | 207 | /* hint: bottom right, only while it has something to say */ | |
| @@ -30,6 +30,9 @@ | |||||
| 30 | .i-split { --i: url(icons/split.svg); } | 30 | .i-split { --i: url(icons/split.svg); } | ||
| 31 | .i-compact { --i: url(icons/compact.svg); } | 31 | .i-compact { --i: url(icons/compact.svg); } | ||
| 32 | .i-mailbox { --i: url(icons/mailbox.svg); } | 32 | .i-mailbox { --i: url(icons/mailbox.svg); } | ||
| 33 | + | .i-sun-moon { --i: url(icons/sun-moon.svg); } | |||
| 34 | + | .i-sun { --i: url(icons/sun.svg); } | |||
| 35 | + | .i-moon { --i: url(icons/moon.svg); } | |||
| 33 | 36 | ||||
| 34 | .pill { | 37 | .pill { | ||
| 35 | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | 38 | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | ||
| @@ -185,7 +188,7 @@ | |||||
| 185 | & .l4, & .l5, & .l6 { padding-left: var(--s6); } | 188 | & .l4, & .l5, & .l6 { padding-left: var(--s6); } | ||
| 186 | } | 189 | } | ||
| 187 | 190 | ||||
| 188 | - | /* footer: version and source (AGPL §13) */ | 191 | + | /* footer: version and source (AGPL §13), then shortcuts and theme */ |
| 189 | .foot { | 192 | .foot { | ||
| 190 | display: flex; flex-wrap: wrap; gap: var(--s4); align-items: center; | 193 | display: flex; flex-wrap: wrap; gap: var(--s4); align-items: center; | ||
| 191 | width: 100%; max-width: var(--page); margin: 0 auto; padding: var(--s4) var(--pad) var(--s5); | 194 | width: 100%; max-width: var(--page); margin: 0 auto; padding: var(--s4) var(--pad) var(--s5); | ||
| @@ -194,6 +197,11 @@ | |||||
| 194 | & a { color: var(--ink-dim); } | 197 | & a { color: var(--ink-dim); } | ||
| 195 | & .foot-keys { margin-left: auto; color: var(--ink-dim); } | 198 | & .foot-keys { margin-left: auto; color: var(--ink-dim); } | ||
| 196 | & .foot-keys:hover { color: var(--ink-hi); } | 199 | & .foot-keys:hover { color: var(--ink-hi); } | ||
| 200 | + | & .foot-theme { | |||
| 201 | + | display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; | |||
| 202 | + | margin: -6px 0; color: var(--ink-dim); border-radius: var(--r-s); | |||
| 203 | + | } | |||
| 204 | + | & .foot-theme:hover { color: var(--ink-hi); background: var(--bg-raised); } | |||
| 197 | } | 205 | } | ||
| 198 | 206 | ||||
| 199 | /* hint: bottom right, only while it has something to say */ | 207 | /* hint: bottom right, only while it has something to say */ | ||
A internal/adapters/web/static/icons/moon.svg +1 -0
| @@ -0,0 +1,1 @@ | |||
| 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="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg> | |
| @@ -0,0 +1,1 @@ | |||||
| 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="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg> | |||
A internal/adapters/web/static/icons/sun-moon.svg +1 -0
| @@ -0,0 +1,1 @@ | |||
| 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 2v2"/><path d="M14.837 16.385a6 6 0 1 1-7.223-7.222c.624-.147.97.66.715 1.248a4 4 0 0 0 5.26 5.259c.589-.255 1.396.09 1.248.715"/><path d="M16 12a4 4 0 0 0-4-4"/><path d="m19 5-1.256 1.256"/><path d="M20 12h2"/></svg> | |
| @@ -0,0 +1,1 @@ | |||||
| 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 2v2"/><path d="M14.837 16.385a6 6 0 1 1-7.223-7.222c.624-.147.97.66.715 1.248a4 4 0 0 0 5.26 5.259c.589-.255 1.396.09 1.248.715"/><path d="M16 12a4 4 0 0 0-4-4"/><path d="m19 5-1.256 1.256"/><path d="M20 12h2"/></svg> | |||
A internal/adapters/web/static/icons/sun.svg +1 -0
| @@ -0,0 +1,1 @@ | |||
| 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"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg> | |
| @@ -0,0 +1,1 @@ | |||||
| 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"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg> | |||
M internal/adapters/web/static/js/ermite.js +31 -0
| @@ -15,6 +15,7 @@ | |||
| 15 | 15 | // 8. Chip lists kept on one line, with a … chip (ermiteFit). | |
| 16 | 16 | // 9. The layout of the home page, remembered in the browser (ermiteLayout). | |
| 17 | 17 | // 10. The wide mode: every page takes the whole width (ermiteWide). | |
| 18 | + | // 11. The theme switch of the footer (ermiteThemeSwitch). | |
| 18 | 19 | // | |
| 19 | 20 | // Preferences are kept by ermiteStore (store.js, loaded first). | |
| 20 | 21 | (() => { | |
| @@ -600,6 +601,36 @@ | |||
| 600 | 601 | return { toggle, apply }; | |
| 601 | 602 | })(); | |
| 602 | 603 | ||
| 604 | + | // The theme switch: the footer button [data-theme-switch] cycles the theme | |
| 605 | + | // of ermiteTheme (store.js, which applies it before the first paint): | |
| 606 | + | // auto → light → dark → auto. Its icon, label and title say the current | |
| 607 | + | // mode and what a click does. Another tab's change applies here at once. | |
| 608 | + | // Self-contained with store.js: jstest/theme.test.mjs evaluates this block. | |
| 609 | + | window.ermiteThemeSwitch = (() => { | |
| 610 | + | const T = window.ermiteTheme; | |
| 611 | + | const icons = { auto: 'i-sun-moon', light: 'i-sun', dark: 'i-moon' }; | |
| 612 | + | function apply() { | |
| 613 | + | const mode = T.apply(); | |
| 614 | + | const label = `theme: ${mode} (click for ${T.next(mode)})`; | |
| 615 | + | for (const b of document.querySelectorAll('[data-theme-switch]')) { | |
| 616 | + | b.setAttribute('aria-label', label); | |
| 617 | + | b.setAttribute('title', label); | |
| 618 | + | const ico = b.querySelector('.ico'); | |
| 619 | + | if (ico) ico.className = 'ico ' + icons[mode]; | |
| 620 | + | } | |
| 621 | + | } | |
| 622 | + | function cycle() { | |
| 623 | + | T.set(T.next(T.get())); | |
| 624 | + | apply(); | |
| 625 | + | } | |
| 626 | + | addEventListener('click', (e) => { | |
| 627 | + | if (e.target.closest?.('[data-theme-switch]')) cycle(); | |
| 628 | + | }); | |
| 629 | + | T.watch(apply); | |
| 630 | + | apply(); | |
| 631 | + | return { cycle, apply }; | |
| 632 | + | })(); | |
| 633 | + | ||
| 603 | 634 | // The menus of the bars (<details class="menu">: the diff view, the merge | |
| 604 | 635 | // menu): closing one hands the focus back to its button when it was inside, | |
| 605 | 636 | // so that a keyboard user keeps their place. | |
| @@ -15,6 +15,7 @@ | |||||
| 15 | // 8. Chip lists kept on one line, with a … chip (ermiteFit). | 15 | // 8. Chip lists kept on one line, with a … chip (ermiteFit). | ||
| 16 | // 9. The layout of the home page, remembered in the browser (ermiteLayout). | 16 | // 9. The layout of the home page, remembered in the browser (ermiteLayout). | ||
| 17 | // 10. The wide mode: every page takes the whole width (ermiteWide). | 17 | // 10. The wide mode: every page takes the whole width (ermiteWide). | ||
| 18 | + | // 11. The theme switch of the footer (ermiteThemeSwitch). | |||
| 18 | // | 19 | // | ||
| 19 | // Preferences are kept by ermiteStore (store.js, loaded first). | 20 | // Preferences are kept by ermiteStore (store.js, loaded first). | ||
| 20 | (() => { | 21 | (() => { | ||
| @@ -600,6 +601,36 @@ | |||||
| 600 | return { toggle, apply }; | 601 | return { toggle, apply }; | ||
| 601 | })(); | 602 | })(); | ||
| 602 | 603 | ||||
| 604 | + | // The theme switch: the footer button [data-theme-switch] cycles the theme | |||
| 605 | + | // of ermiteTheme (store.js, which applies it before the first paint): | |||
| 606 | + | // auto → light → dark → auto. Its icon, label and title say the current | |||
| 607 | + | // mode and what a click does. Another tab's change applies here at once. | |||
| 608 | + | // Self-contained with store.js: jstest/theme.test.mjs evaluates this block. | |||
| 609 | + | window.ermiteThemeSwitch = (() => { | |||
| 610 | + | const T = window.ermiteTheme; | |||
| 611 | + | const icons = { auto: 'i-sun-moon', light: 'i-sun', dark: 'i-moon' }; | |||
| 612 | + | function apply() { | |||
| 613 | + | const mode = T.apply(); | |||
| 614 | + | const label = `theme: ${mode} (click for ${T.next(mode)})`; | |||
| 615 | + | for (const b of document.querySelectorAll('[data-theme-switch]')) { | |||
| 616 | + | b.setAttribute('aria-label', label); | |||
| 617 | + | b.setAttribute('title', label); | |||
| 618 | + | const ico = b.querySelector('.ico'); | |||
| 619 | + | if (ico) ico.className = 'ico ' + icons[mode]; | |||
| 620 | + | } | |||
| 621 | + | } | |||
| 622 | + | function cycle() { | |||
| 623 | + | T.set(T.next(T.get())); | |||
| 624 | + | apply(); | |||
| 625 | + | } | |||
| 626 | + | addEventListener('click', (e) => { | |||
| 627 | + | if (e.target.closest?.('[data-theme-switch]')) cycle(); | |||
| 628 | + | }); | |||
| 629 | + | T.watch(apply); | |||
| 630 | + | apply(); | |||
| 631 | + | return { cycle, apply }; | |||
| 632 | + | })(); | |||
| 633 | + | ||||
| 603 | // The menus of the bars (<details class="menu">: the diff view, the merge | 634 | // The menus of the bars (<details class="menu">: the diff view, the merge | ||
| 604 | // menu): closing one hands the focus back to its button when it was inside, | 635 | // menu): closing one hands the focus back to its button when it was inside, | ||
| 605 | // so that a keyboard user keeps their place. | 636 | // so that a keyboard user keeps their place. | ||
M internal/adapters/web/static/js/store.js +52 -0
| @@ -19,6 +19,9 @@ | |||
| 19 | 19 | // set ignores what is not | |
| 20 | 20 | // | |
| 21 | 21 | // Self-contained: jstest/store.test.mjs evaluates this file alone. | |
| 22 | + | // | |
| 23 | + | // It also sets, before the first paint, what the stored preferences change on | |
| 24 | + | // <html>: the wide mode and the theme (window.ermiteTheme, below). | |
| 22 | 25 | window.ermiteStore = (() => { | |
| 23 | 26 | // What storage could not keep, by key. A key mapped to null is removed for | |
| 24 | 27 | // this page although storage would not say so. | |
| @@ -79,3 +82,52 @@ window.ermiteStore = (() => { | |||
| 79 | 82 | // shell.templ loads this file without defer, so a wide page never draws narrow | |
| 80 | 83 | // first. | |
| 81 | 84 | if (window.ermiteStore.get('wide') === '1') document?.documentElement?.classList?.add('wide'); | |
| 85 | + | ||
| 86 | + | // window.ermiteTheme: the colour theme, "auto" (the system's, the default), | |
| 87 | + | // "light" or "dark", kept by ermiteStore ("theme"). A forced theme is | |
| 88 | + | // html[data-theme], which sets color-scheme (00-tokens.css); the | |
| 89 | + | // color-scheme and theme-color metas follow so the browser's own UI matches. | |
| 90 | + | // Applied here, before the first paint: the page never draws in the wrong | |
| 91 | + | // theme first. The footer button (ermiteThemeSwitch in ermite.js) cycles it. | |
| 92 | + | // | |
| 93 | + | // get() the mode | |
| 94 | + | // next(mode) the mode after it: auto → light → dark → auto | |
| 95 | + | // set(mode) stores and applies it | |
| 96 | + | // apply() shows the stored mode on the page | |
| 97 | + | // watch(fn) fn(mode) when another tab changes it | |
| 98 | + | // | |
| 99 | + | // Tested in Node by jstest/theme.test.mjs. | |
| 100 | + | window.ermiteTheme = (() => { | |
| 101 | + | const values = ['auto', 'light', 'dark']; | |
| 102 | + | const pref = window.ermiteStore.pref('theme', { values, fallback: 'auto' }); | |
| 103 | + | const next = (mode) => values[(values.indexOf(mode) + 1) % values.length]; | |
| 104 | + | ||
| 105 | + | function apply() { | |
| 106 | + | const mode = pref.get(); | |
| 107 | + | const doc = globalThis.document; | |
| 108 | + | const root = doc?.documentElement; | |
| 109 | + | if (!root) return mode; | |
| 110 | + | if (mode === 'auto') root.removeAttribute?.('data-theme'); | |
| 111 | + | else root.setAttribute?.('data-theme', mode); | |
| 112 | + | doc.querySelector?.('meta[name="color-scheme"]') | |
| 113 | + | ?.setAttribute('content', mode === 'auto' ? 'light dark' : mode); | |
| 114 | + | // Each theme-color meta keeps its rendered colour in data-auto. Forced, | |
| 115 | + | // both metas take the colour of the meta of that scheme, whatever the | |
| 116 | + | // system scheme their media matches. | |
| 117 | + | const metas = [...(doc.querySelectorAll?.('meta[name="theme-color"]') ?? [])]; | |
| 118 | + | for (const m of metas) if (!m.hasAttribute('data-auto')) m.setAttribute('data-auto', m.getAttribute('content') ?? ''); | |
| 119 | + | const forced = metas.find((m) => (m.getAttribute('media') ?? '').includes('prefers-color-scheme: ' + mode)); | |
| 120 | + | for (const m of metas) { | |
| 121 | + | m.setAttribute('content', mode === 'auto' || !forced ? m.getAttribute('data-auto') : forced.getAttribute('data-auto')); | |
| 122 | + | } | |
| 123 | + | return mode; | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | function set(mode) { | |
| 127 | + | pref.set(mode); | |
| 128 | + | apply(); | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | apply(); | |
| 132 | + | return { values, get: pref.get, next, set, apply, watch: pref.watch }; | |
| 133 | + | })(); | |
| @@ -19,6 +19,9 @@ | |||||
| 19 | // set ignores what is not | 19 | // set ignores what is not | ||
| 20 | // | 20 | // | ||
| 21 | // Self-contained: jstest/store.test.mjs evaluates this file alone. | 21 | // Self-contained: jstest/store.test.mjs evaluates this file alone. | ||
| 22 | + | // | |||
| 23 | + | // It also sets, before the first paint, what the stored preferences change on | |||
| 24 | + | // <html>: the wide mode and the theme (window.ermiteTheme, below). | |||
| 22 | window.ermiteStore = (() => { | 25 | window.ermiteStore = (() => { | ||
| 23 | // What storage could not keep, by key. A key mapped to null is removed for | 26 | // What storage could not keep, by key. A key mapped to null is removed for | ||
| 24 | // this page although storage would not say so. | 27 | // this page although storage would not say so. | ||
| @@ -79,3 +82,52 @@ window.ermiteStore = (() => { | |||||
| 79 | // shell.templ loads this file without defer, so a wide page never draws narrow | 82 | // shell.templ loads this file without defer, so a wide page never draws narrow | ||
| 80 | // first. | 83 | // first. | ||
| 81 | if (window.ermiteStore.get('wide') === '1') document?.documentElement?.classList?.add('wide'); | 84 | if (window.ermiteStore.get('wide') === '1') document?.documentElement?.classList?.add('wide'); | ||
| 85 | + | ||||
| 86 | + | // window.ermiteTheme: the colour theme, "auto" (the system's, the default), | |||
| 87 | + | // "light" or "dark", kept by ermiteStore ("theme"). A forced theme is | |||
| 88 | + | // html[data-theme], which sets color-scheme (00-tokens.css); the | |||
| 89 | + | // color-scheme and theme-color metas follow so the browser's own UI matches. | |||
| 90 | + | // Applied here, before the first paint: the page never draws in the wrong | |||
| 91 | + | // theme first. The footer button (ermiteThemeSwitch in ermite.js) cycles it. | |||
| 92 | + | // | |||
| 93 | + | // get() the mode | |||
| 94 | + | // next(mode) the mode after it: auto → light → dark → auto | |||
| 95 | + | // set(mode) stores and applies it | |||
| 96 | + | // apply() shows the stored mode on the page | |||
| 97 | + | // watch(fn) fn(mode) when another tab changes it | |||
| 98 | + | // | |||
| 99 | + | // Tested in Node by jstest/theme.test.mjs. | |||
| 100 | + | window.ermiteTheme = (() => { | |||
| 101 | + | const values = ['auto', 'light', 'dark']; | |||
| 102 | + | const pref = window.ermiteStore.pref('theme', { values, fallback: 'auto' }); | |||
| 103 | + | const next = (mode) => values[(values.indexOf(mode) + 1) % values.length]; | |||
| 104 | + | ||||
| 105 | + | function apply() { | |||
| 106 | + | const mode = pref.get(); | |||
| 107 | + | const doc = globalThis.document; | |||
| 108 | + | const root = doc?.documentElement; | |||
| 109 | + | if (!root) return mode; | |||
| 110 | + | if (mode === 'auto') root.removeAttribute?.('data-theme'); | |||
| 111 | + | else root.setAttribute?.('data-theme', mode); | |||
| 112 | + | doc.querySelector?.('meta[name="color-scheme"]') | |||
| 113 | + | ?.setAttribute('content', mode === 'auto' ? 'light dark' : mode); | |||
| 114 | + | // Each theme-color meta keeps its rendered colour in data-auto. Forced, | |||
| 115 | + | // both metas take the colour of the meta of that scheme, whatever the | |||
| 116 | + | // system scheme their media matches. | |||
| 117 | + | const metas = [...(doc.querySelectorAll?.('meta[name="theme-color"]') ?? [])]; | |||
| 118 | + | for (const m of metas) if (!m.hasAttribute('data-auto')) m.setAttribute('data-auto', m.getAttribute('content') ?? ''); | |||
| 119 | + | const forced = metas.find((m) => (m.getAttribute('media') ?? '').includes('prefers-color-scheme: ' + mode)); | |||
| 120 | + | for (const m of metas) { | |||
| 121 | + | m.setAttribute('content', mode === 'auto' || !forced ? m.getAttribute('data-auto') : forced.getAttribute('data-auto')); | |||
| 122 | + | } | |||
| 123 | + | return mode; | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | function set(mode) { | |||
| 127 | + | pref.set(mode); | |||
| 128 | + | apply(); | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | apply(); | |||
| 132 | + | return { values, get: pref.get, next, set, apply, watch: pref.watch }; | |||
| 133 | + | })(); | |||
M internal/app/testdata/web/blob-binary.html +4 -0
| @@ -140,6 +140,10 @@ | |||
| 140 | 140 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 141 | 141 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 142 | 142 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 143 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 144 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 145 | + | </span> | |
| 146 | + | </button> | |
| 143 | 147 | </footer> | |
| 144 | 148 | <div class="hint"> | |
| 145 | 149 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -140,6 +140,10 @@ | |||||
| 140 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 140 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 141 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 141 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 142 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 142 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 143 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 144 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 145 | + | </span> | |||
| 146 | + | </button> | |||
| 143 | </footer> | 147 | </footer> | ||
| 144 | <div class="hint"> | 148 | <div class="hint"> | ||
| 145 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 149 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/blob-code.html +4 -0
| @@ -210,6 +210,10 @@ | |||
| 210 | 210 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 211 | 211 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 212 | 212 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 213 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 214 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 215 | + | </span> | |
| 216 | + | </button> | |
| 213 | 217 | </footer> | |
| 214 | 218 | <div class="hint"> | |
| 215 | 219 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -210,6 +210,10 @@ | |||||
| 210 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 210 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 211 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 211 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 212 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 212 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 213 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 214 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 215 | + | </span> | |||
| 216 | + | </button> | |||
| 213 | </footer> | 217 | </footer> | ||
| 214 | <div class="hint"> | 218 | <div class="hint"> | ||
| 215 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 219 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/blob-empty.html +4 -0
| @@ -140,6 +140,10 @@ | |||
| 140 | 140 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 141 | 141 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 142 | 142 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 143 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 144 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 145 | + | </span> | |
| 146 | + | </button> | |
| 143 | 147 | </footer> | |
| 144 | 148 | <div class="hint"> | |
| 145 | 149 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -140,6 +140,10 @@ | |||||
| 140 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 140 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 141 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 141 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 142 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 142 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 143 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 144 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 145 | + | </span> | |||
| 146 | + | </button> | |||
| 143 | </footer> | 147 | </footer> | ||
| 144 | <div class="hint"> | 148 | <div class="hint"> | ||
| 145 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 149 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/blob-image.html +4 -0
| @@ -142,6 +142,10 @@ | |||
| 142 | 142 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 143 | 143 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 144 | 144 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 145 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 146 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 147 | + | </span> | |
| 148 | + | </button> | |
| 145 | 149 | </footer> | |
| 146 | 150 | <div class="hint"> | |
| 147 | 151 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -142,6 +142,10 @@ | |||||
| 142 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 142 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 143 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 143 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 144 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 144 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 145 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 146 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 147 | + | </span> | |||
| 148 | + | </button> | |||
| 145 | </footer> | 149 | </footer> | ||
| 146 | <div class="hint"> | 150 | <div class="hint"> | ||
| 147 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 151 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/blob-markdown.html +4 -0
| @@ -207,6 +207,10 @@ | |||
| 207 | 207 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 208 | 208 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 209 | 209 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 210 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 211 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 212 | + | </span> | |
| 213 | + | </button> | |
| 210 | 214 | </footer> | |
| 211 | 215 | <div class="hint"> | |
| 212 | 216 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -207,6 +207,10 @@ | |||||
| 207 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 207 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 208 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 208 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 209 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 209 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 210 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 211 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 212 | + | </span> | |||
| 213 | + | </button> | |||
| 210 | </footer> | 214 | </footer> | ||
| 211 | <div class="hint"> | 215 | <div class="hint"> | ||
| 212 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 216 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/blob-svg.html +4 -0
| @@ -165,6 +165,10 @@ | |||
| 165 | 165 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 166 | 166 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 167 | 167 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 168 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 169 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 170 | + | </span> | |
| 171 | + | </button> | |
| 168 | 172 | </footer> | |
| 169 | 173 | <div class="hint"> | |
| 170 | 174 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -165,6 +165,10 @@ | |||||
| 165 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 165 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 166 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 166 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 167 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 167 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 168 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 169 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 170 | + | </span> | |||
| 171 | + | </button> | |||
| 168 | </footer> | 172 | </footer> | ||
| 169 | <div class="hint"> | 173 | <div class="hint"> | ||
| 170 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 174 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/blob-text.html +4 -0
| @@ -160,6 +160,10 @@ | |||
| 160 | 160 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 161 | 161 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 162 | 162 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 163 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 164 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 165 | + | </span> | |
| 166 | + | </button> | |
| 163 | 167 | </footer> | |
| 164 | 168 | <div class="hint"> | |
| 165 | 169 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -160,6 +160,10 @@ | |||||
| 160 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 160 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 161 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 161 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 162 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 162 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 163 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 164 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 165 | + | </span> | |||
| 166 | + | </button> | |||
| 163 | </footer> | 167 | </footer> | ||
| 164 | <div class="hint"> | 168 | <div class="hint"> | ||
| 165 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 169 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/blob-too-large.html +4 -0
| @@ -140,6 +140,10 @@ | |||
| 140 | 140 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 141 | 141 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 142 | 142 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 143 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 144 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 145 | + | </span> | |
| 146 | + | </button> | |
| 143 | 147 | </footer> | |
| 144 | 148 | <div class="hint"> | |
| 145 | 149 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -140,6 +140,10 @@ | |||||
| 140 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 140 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 141 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 141 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 142 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 142 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 143 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 144 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 145 | + | </span> | |||
| 146 | + | </button> | |||
| 143 | </footer> | 147 | </footer> | ||
| 144 | <div class="hint"> | 148 | <div class="hint"> | ||
| 145 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 149 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/blob-trojan.html +4 -0
| @@ -207,6 +207,10 @@ | |||
| 207 | 207 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 208 | 208 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 209 | 209 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 210 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 211 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 212 | + | </span> | |
| 213 | + | </button> | |
| 210 | 214 | </footer> | |
| 211 | 215 | <div class="hint"> | |
| 212 | 216 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -207,6 +207,10 @@ | |||||
| 207 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 207 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 208 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 208 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 209 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 209 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 210 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 211 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 212 | + | </span> | |||
| 213 | + | </button> | |||
| 210 | </footer> | 214 | </footer> | ||
| 211 | <div class="hint"> | 215 | <div class="hint"> | ||
| 212 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 216 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commit-big.html +4 -0
| @@ -192,6 +192,10 @@ | |||
| 192 | 192 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 193 | 193 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 194 | 194 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 195 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 196 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 197 | + | </span> | |
| 198 | + | </button> | |
| 195 | 199 | </footer> | |
| 196 | 200 | <div class="hint"> | |
| 197 | 201 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -192,6 +192,10 @@ | |||||
| 192 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 192 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 193 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 193 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 194 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 194 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 195 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 196 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 197 | + | </span> | |||
| 198 | + | </button> | |||
| 195 | </footer> | 199 | </footer> | ||
| 196 | <div class="hint"> | 200 | <div class="hint"> | ||
| 197 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 201 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commit-binary.html +4 -0
| @@ -183,6 +183,10 @@ | |||
| 183 | 183 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 184 | 184 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 185 | 185 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 186 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 187 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 188 | + | </span> | |
| 189 | + | </button> | |
| 186 | 190 | </footer> | |
| 187 | 191 | <div class="hint"> | |
| 188 | 192 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -183,6 +183,10 @@ | |||||
| 183 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 183 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 184 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 184 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 185 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 185 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 186 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 187 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 188 | + | </span> | |||
| 189 | + | </button> | |||
| 186 | </footer> | 190 | </footer> | ||
| 187 | <div class="hint"> | 191 | <div class="hint"> | ||
| 188 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 192 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commit-merge.html +4 -0
| @@ -225,6 +225,10 @@ | |||
| 225 | 225 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 226 | 226 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 227 | 227 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 228 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 229 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 230 | + | </span> | |
| 231 | + | </button> | |
| 228 | 232 | </footer> | |
| 229 | 233 | <div class="hint"> | |
| 230 | 234 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -225,6 +225,10 @@ | |||||
| 225 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 225 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 226 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 226 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 227 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 227 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 228 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 229 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 230 | + | </span> | |||
| 231 | + | </button> | |||
| 228 | </footer> | 232 | </footer> | ||
| 229 | <div class="hint"> | 233 | <div class="hint"> | ||
| 230 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 234 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commit-modify-split.html +4 -0
| @@ -348,6 +348,10 @@ And <b>html</b> & "quotes".</pre> | |||
| 348 | 348 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 349 | 349 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 350 | 350 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 351 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 352 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 353 | + | </span> | |
| 354 | + | </button> | |
| 351 | 355 | </footer> | |
| 352 | 356 | <div class="hint"> | |
| 353 | 357 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -348,6 +348,10 @@ And <b>html</b> & "quotes".</pre> | |||||
| 348 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 348 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 349 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 349 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 350 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 350 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 351 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 352 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 353 | + | </span> | |||
| 354 | + | </button> | |||
| 351 | </footer> | 355 | </footer> | ||
| 352 | <div class="hint"> | 356 | <div class="hint"> | ||
| 353 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 357 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commit-modify.html +4 -0
| @@ -348,6 +348,10 @@ And <b>html</b> & "quotes".</pre> | |||
| 348 | 348 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 349 | 349 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 350 | 350 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 351 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 352 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 353 | + | </span> | |
| 354 | + | </button> | |
| 351 | 355 | </footer> | |
| 352 | 356 | <div class="hint"> | |
| 353 | 357 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -348,6 +348,10 @@ And <b>html</b> & "quotes".</pre> | |||||
| 348 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 348 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 349 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 349 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 350 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 350 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 351 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 352 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 353 | + | </span> | |||
| 354 | + | </button> | |||
| 351 | </footer> | 355 | </footer> | ||
| 352 | <div class="hint"> | 356 | <div class="hint"> | ||
| 353 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 357 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commit-rename.html +4 -0
| @@ -252,6 +252,10 @@ | |||
| 252 | 252 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 253 | 253 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 254 | 254 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 255 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 256 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 257 | + | </span> | |
| 258 | + | </button> | |
| 255 | 259 | </footer> | |
| 256 | 260 | <div class="hint"> | |
| 257 | 261 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -252,6 +252,10 @@ | |||||
| 252 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 252 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 253 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 253 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 254 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 254 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 255 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 256 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 257 | + | </span> | |||
| 258 | + | </button> | |||
| 255 | </footer> | 259 | </footer> | ||
| 256 | <div class="hint"> | 260 | <div class="hint"> | ||
| 257 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 261 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commit-root.html +4 -0
| @@ -571,6 +571,10 @@ | |||
| 571 | 571 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 572 | 572 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 573 | 573 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 574 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 575 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 576 | + | </span> | |
| 577 | + | </button> | |
| 574 | 578 | </footer> | |
| 575 | 579 | <div class="hint"> | |
| 576 | 580 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -571,6 +571,10 @@ | |||||
| 571 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 571 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 572 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 572 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 573 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 573 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 574 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 575 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 576 | + | </span> | |||
| 577 | + | </button> | |||
| 574 | </footer> | 578 | </footer> | ||
| 575 | <div class="hint"> | 579 | <div class="hint"> | ||
| 576 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 580 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commit-trojan.html +4 -0
| @@ -400,6 +400,10 @@ | |||
| 400 | 400 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 401 | 401 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 402 | 402 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 403 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 404 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 405 | + | </span> | |
| 406 | + | </button> | |
| 403 | 407 | </footer> | |
| 404 | 408 | <div class="hint"> | |
| 405 | 409 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -400,6 +400,10 @@ | |||||
| 400 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 400 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 401 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 401 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 402 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 402 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 403 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 404 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 405 | + | </span> | |||
| 406 | + | </button> | |||
| 403 | </footer> | 407 | </footer> | ||
| 404 | <div class="hint"> | 408 | <div class="hint"> | ||
| 405 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 409 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commits-page2.html +4 -0
| @@ -167,6 +167,10 @@ | |||
| 167 | 167 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 168 | 168 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 169 | 169 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 170 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 171 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 172 | + | </span> | |
| 173 | + | </button> | |
| 170 | 174 | </footer> | |
| 171 | 175 | <div class="hint"> | |
| 172 | 176 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -167,6 +167,10 @@ | |||||
| 167 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 167 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 168 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 168 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 169 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 169 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 170 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 171 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 172 | + | </span> | |||
| 173 | + | </button> | |||
| 170 | </footer> | 174 | </footer> | ||
| 171 | <div class="hint"> | 175 | <div class="hint"> | ||
| 172 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 176 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commits-path.html +4 -0
| @@ -182,6 +182,10 @@ | |||
| 182 | 182 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 183 | 183 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 184 | 184 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 185 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 186 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 187 | + | </span> | |
| 188 | + | </button> | |
| 185 | 189 | </footer> | |
| 186 | 190 | <div class="hint"> | |
| 187 | 191 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -182,6 +182,10 @@ | |||||
| 182 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 182 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 183 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 183 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 184 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 184 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 185 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 186 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 187 | + | </span> | |||
| 188 | + | </button> | |||
| 185 | </footer> | 189 | </footer> | ||
| 186 | <div class="hint"> | 190 | <div class="hint"> | ||
| 187 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 191 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commits-root.html +4 -0
| @@ -964,6 +964,10 @@ | |||
| 964 | 964 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 965 | 965 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 966 | 966 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 967 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 968 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 969 | + | </span> | |
| 970 | + | </button> | |
| 967 | 971 | </footer> | |
| 968 | 972 | <div class="hint"> | |
| 969 | 973 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -964,6 +964,10 @@ | |||||
| 964 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 964 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 965 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 965 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 966 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 966 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 967 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 968 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 969 | + | </span> | |||
| 970 | + | </button> | |||
| 967 | </footer> | 971 | </footer> | ||
| 968 | <div class="hint"> | 972 | <div class="hint"> | ||
| 969 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 973 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/commits-small.html +4 -0
| @@ -309,6 +309,10 @@ | |||
| 309 | 309 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 310 | 310 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 311 | 311 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 312 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 313 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 314 | + | </span> | |
| 315 | + | </button> | |
| 312 | 316 | </footer> | |
| 313 | 317 | <div class="hint"> | |
| 314 | 318 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -309,6 +309,10 @@ | |||||
| 309 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 309 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 310 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 310 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 311 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 311 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 312 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 313 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 314 | + | </span> | |||
| 315 | + | </button> | |||
| 312 | </footer> | 316 | </footer> | ||
| 313 | <div class="hint"> | 317 | <div class="hint"> | ||
| 314 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 318 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/empty.html +4 -0
| @@ -85,6 +85,10 @@ | |||
| 85 | 85 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 86 | 86 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 87 | 87 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 88 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 89 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 90 | + | </span> | |
| 91 | + | </button> | |
| 88 | 92 | </footer> | |
| 89 | 93 | <div class="hint"> | |
| 90 | 94 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -85,6 +85,10 @@ | |||||
| 85 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 85 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 86 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 86 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 87 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 87 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 88 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 89 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 90 | + | </span> | |||
| 91 | + | </button> | |||
| 88 | </footer> | 92 | </footer> | ||
| 89 | <div class="hint"> | 93 | <div class="hint"> | ||
| 90 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 94 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/home.html +4 -0
| @@ -181,6 +181,10 @@ | |||
| 181 | 181 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 182 | 182 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 183 | 183 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 184 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 185 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 186 | + | </span> | |
| 187 | + | </button> | |
| 184 | 188 | </footer> | |
| 185 | 189 | <div class="hint"> | |
| 186 | 190 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -181,6 +181,10 @@ | |||||
| 181 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 181 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 182 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 182 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 183 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 183 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 184 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 185 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 186 | + | </span> | |||
| 187 | + | </button> | |||
| 184 | </footer> | 188 | </footer> | ||
| 185 | <div class="hint"> | 189 | <div class="hint"> | ||
| 186 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 190 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/not-found-page.html +4 -0
| @@ -74,6 +74,10 @@ | |||
| 74 | 74 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 75 | 75 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 76 | 76 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 77 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 78 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 79 | + | </span> | |
| 80 | + | </button> | |
| 77 | 81 | </footer> | |
| 78 | 82 | <div class="hint"> | |
| 79 | 83 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -74,6 +74,10 @@ | |||||
| 74 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 74 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 75 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 75 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 76 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 76 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 77 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 78 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 79 | + | </span> | |||
| 80 | + | </button> | |||
| 77 | </footer> | 81 | </footer> | ||
| 78 | <div class="hint"> | 82 | <div class="hint"> | ||
| 79 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 83 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/not-found.html +4 -0
| @@ -74,6 +74,10 @@ | |||
| 74 | 74 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 75 | 75 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 76 | 76 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 77 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 78 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 79 | + | </span> | |
| 80 | + | </button> | |
| 77 | 81 | </footer> | |
| 78 | 82 | <div class="hint"> | |
| 79 | 83 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -74,6 +74,10 @@ | |||||
| 74 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 74 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 75 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 75 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 76 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 76 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 77 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 78 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 79 | + | </span> | |||
| 80 | + | </button> | |||
| 77 | </footer> | 81 | </footer> | ||
| 78 | <div class="hint"> | 82 | <div class="hint"> | ||
| 79 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 83 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/refs.html +4 -0
| @@ -191,6 +191,10 @@ | |||
| 191 | 191 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 192 | 192 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 193 | 193 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 194 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 195 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 196 | + | </span> | |
| 197 | + | </button> | |
| 194 | 198 | </footer> | |
| 195 | 199 | <div class="hint"> | |
| 196 | 200 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -191,6 +191,10 @@ | |||||
| 191 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 191 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 192 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 192 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 193 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 193 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 194 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 195 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 196 | + | </span> | |||
| 197 | + | </button> | |||
| 194 | </footer> | 198 | </footer> | ||
| 195 | <div class="hint"> | 199 | <div class="hint"> | ||
| 196 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 200 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/summary.html +4 -0
| @@ -217,6 +217,10 @@ | |||
| 217 | 217 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 218 | 218 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 219 | 219 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 220 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 221 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 222 | + | </span> | |
| 223 | + | </button> | |
| 220 | 224 | </footer> | |
| 221 | 225 | <div class="hint"> | |
| 222 | 226 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -217,6 +217,10 @@ | |||||
| 217 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 217 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 218 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 218 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 219 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 219 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 220 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 221 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 222 | + | </span> | |||
| 223 | + | </button> | |||
| 220 | </footer> | 224 | </footer> | ||
| 221 | <div class="hint"> | 225 | <div class="hint"> | ||
| 222 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 226 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/tree-nested.html +4 -0
| @@ -155,6 +155,10 @@ | |||
| 155 | 155 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 156 | 156 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 157 | 157 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 158 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 159 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 160 | + | </span> | |
| 161 | + | </button> | |
| 158 | 162 | </footer> | |
| 159 | 163 | <div class="hint"> | |
| 160 | 164 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -155,6 +155,10 @@ | |||||
| 155 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 155 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 156 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 156 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 157 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 157 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 158 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 159 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 160 | + | </span> | |||
| 161 | + | </button> | |||
| 158 | </footer> | 162 | </footer> | ||
| 159 | <div class="hint"> | 163 | <div class="hint"> | ||
| 160 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 164 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/tree-root.html +4 -0
| @@ -376,6 +376,10 @@ | |||
| 376 | 376 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 377 | 377 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 378 | 378 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 379 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 380 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 381 | + | </span> | |
| 382 | + | </button> | |
| 379 | 383 | </footer> | |
| 380 | 384 | <div class="hint"> | |
| 381 | 385 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -376,6 +376,10 @@ | |||||
| 376 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 376 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 377 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 377 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 378 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 378 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 379 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 380 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 381 | + | </span> | |||
| 382 | + | </button> | |||
| 379 | </footer> | 383 | </footer> | ||
| 380 | <div class="hint"> | 384 | <div class="hint"> | ||
| 381 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 385 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/tree-slash-ref.html +4 -0
| @@ -152,6 +152,10 @@ | |||
| 152 | 152 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 153 | 153 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 154 | 154 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 155 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 156 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 157 | + | </span> | |
| 158 | + | </button> | |
| 155 | 159 | </footer> | |
| 156 | 160 | <div class="hint"> | |
| 157 | 161 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -152,6 +152,10 @@ | |||||
| 152 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 152 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 153 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 153 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 154 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 154 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 155 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 156 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 157 | + | </span> | |||
| 158 | + | </button> | |||
| 155 | </footer> | 159 | </footer> | ||
| 156 | <div class="hint"> | 160 | <div class="hint"> | ||
| 157 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 161 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||
M internal/app/testdata/web/tree-subdir.html +4 -0
| @@ -168,6 +168,10 @@ | |||
| 168 | 168 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |
| 169 | 169 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | |
| 170 | 170 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | |
| 171 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |
| 172 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |
| 173 | + | </span> | |
| 174 | + | </button> | |
| 171 | 175 | </footer> | |
| 172 | 176 | <div class="hint"> | |
| 173 | 177 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | |
| @@ -168,6 +168,10 @@ | |||||
| 168 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | 168 | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | ||
| 169 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | 169 | <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?"> | ||
| 170 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | 170 | <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button> | ||
| 171 | + | <button type="button" class="foot-theme" data-js data-theme-switch aria-label="theme: auto (click for light)" title="theme: auto (click for light)"> | |||
| 172 | + | <span class="ico i-sun-moon" aria-hidden="true"> | |||
| 173 | + | </span> | |||
| 174 | + | </button> | |||
| 171 | </footer> | 175 | </footer> | ||
| 172 | <div class="hint"> | 176 | <div class="hint"> | ||
| 173 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | 177 | <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"> | ||