feat(web): rework the interface with surfaces, a two-level header and a command palette
Replace the terminal-style bars with a header (instance, path, ref switcher, go-to palette, owner) and repository tabs that show their key. Pages use grey surface levels, cards for the README and the rail blocks, an 8px spacing scale, and embedded Atkinson Hyperlegible Next and Mono fonts. The palette, ref switcher and keys are centred modal dialogs (ctrl k opens the palette). The bottom bar becomes a footer with the version and source link, and a hint badge that shows the row position, the line selection, flash messages and a lost event stream. The rail moves under the content on narrow screens. Pages are prefetched on hover through speculation rules and cross-fade between documents.
- commit
- c48540b27ef91bcc0838d09f16cdcefe39a494c2
- author
- brice · · 2026-10-11 10:28 UTC
- parent
- 3027a40
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
98 files · +6883 -5112
M cmd/ermite/testdata/web/blob-binary.html +162 -114
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>data.bin · files · example.test</title> | 7 | <title>data.bin · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,45 +42,62 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">main</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 68 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 72 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work has-rail"> | 86 | <div class="work has-rail"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <h1 class="file-h"> | 88 | <h1 class="file-h"> | ||
| 66 | <span>data.bin</span> | 89 | <span>data.bin</span> | ||
| 67 | <span class="acts"> | 90 | <span class="acts"> | ||
| 68 | - | <a href="/files/raw/main/data.bin">raw</a> | 91 | + | <a class="btn" href="/files/raw/main/data.bin">Raw</a> |
| 69 | - | <a href="/files/commits/main/data.bin">history</a> | 92 | + | <a class="btn" href="/files/commits/main/data.bin">History</a> |
| 70 | - | <a href="/files/tree/main">dir</a> | 93 | + | <a class="btn" href="/files/tree/main">Folder</a> |
| 71 | </span> | 94 | </span> | ||
| 72 | </h1> | 95 | </h1> | ||
| 73 | <p class="notice" role="note">binary file (application/octet-stream, 10 B): not shown. use raw.</p> | 96 | <p class="notice" role="note">binary file (application/octet-stream, 10 B): not shown. use raw.</p> | ||
| 74 | </main> | 97 | </main> | ||
| 75 | <aside class="rail" aria-label="Context"> | 98 | <aside class="rail" aria-label="Context"> | ||
| 76 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 77 | - | <summary>file · last change · selection</summary> | |||
| 78 | <section aria-labelledby="r-file"> | 99 | <section aria-labelledby="r-file"> | ||
| 79 | - | <h2 id="r-file">file</h2> | 100 | + | <h2 id="r-file">File</h2> |
| 80 | <dl class="kv"> | 101 | <dl class="kv"> | ||
| 81 | <dt>size</dt> | 102 | <dt>size</dt> | ||
| 82 | <dd>10 B</dd> | 103 | <dd>10 B</dd> | ||
| @@ -85,80 +106,80 @@ | |||||
| 85 | </dl> | 106 | </dl> | ||
| 86 | </section> | 107 | </section> | ||
| 87 | <section aria-labelledby="r-last"> | 108 | <section aria-labelledby="r-last"> | ||
| 88 | - | <h2 id="r-last">last change <a href="/files/commits/main/data.bin">history</a> | 109 | + | <h2 id="r-last">Last change <a href="/files/commits/main/data.bin">History</a> |
| 89 | </h2> | 110 | </h2> | ||
| 90 | <p> | 111 | <p> | ||
| 91 | <a href="/files/commit/SHA40">SHA7</a> add data.bin</p> | 112 | <a href="/files/commit/SHA40">SHA7</a> add data.bin</p> | ||
| 92 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | 113 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 93 | </p> | 114 | </p> | ||
| 94 | </section> | 115 | </section> | ||
| 95 | - | </details> | |||
| 96 | </aside> | 116 | </aside> | ||
| 97 | </div> | 117 | </div> | ||
| 98 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 118 | + | <footer class="foot"> |
| 119 | + | <span>ermite devel</span> | |||
| 120 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 121 | + | <span class="foot-keys"> | |||
| 122 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 123 | + | </footer> | |||
| 124 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 125 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 126 | + | </span> | |||
| 127 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 128 | + | </span> | |||
| 129 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 130 | + | </span> | |||
| 131 | + | </div> | |||
| 132 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 99 | <div class="panel-head"> | 133 | <div class="panel-head"> | ||
| 100 | - | <span>keys</span> | 134 | + | <h2>Keyboard shortcuts</h2> |
| 101 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 135 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 136 | + | <kbd>esc</kbd> close</button> | |||
| 102 | </div> | 137 | </div> | ||
| 103 | <dl class="keys"> | 138 | <dl class="keys"> | ||
| 104 | <div> | 139 | <div> | ||
| 105 | <dt> | 140 | <dt> | ||
| 106 | - | <kbd>j</kbd> | 141 | + | <kbd>ctrl k</kbd> |
| 107 | - | <kbd>k</kbd> | 142 | + | <kbd>:</kbd> |
| 108 | </dt> | 143 | </dt> | ||
| 109 | - | <dd>move cursor down / up</dd> | 144 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 110 | </div> | 145 | </div> | ||
| 111 | <div> | 146 | <div> | ||
| 112 | <dt> | 147 | <dt> | ||
| 113 | - | <kbd>enter</kbd> | 148 | + | <kbd>?</kbd> |
| 114 | </dt> | 149 | </dt> | ||
| 115 | - | <dd>open row under cursor</dd> | 150 | + | <dd>this list</dd> |
| 116 | </div> | 151 | </div> | ||
| 117 | <div> | 152 | <div> | ||
| 118 | <dt> | 153 | <dt> | ||
| 119 | - | <kbd>g</kbd> | 154 | + | <kbd>j</kbd> |
| 120 | - | <kbd>g</kbd> / <kbd>G</kbd> | 155 | + | <kbd>k</kbd> |
| 121 | </dt> | 156 | </dt> | ||
| 122 | - | <dd>first / last row</dd> | 157 | + | <dd>move the row cursor down / up</dd> |
| 123 | </div> | 158 | </div> | ||
| 124 | <div> | 159 | <div> | ||
| 125 | <dt> | 160 | <dt> | ||
| 126 | - | <kbd>/</kbd> | 161 | + | <kbd>enter</kbd> |
| 127 | </dt> | 162 | </dt> | ||
| 128 | - | <dd>focus the filter</dd> | 163 | + | <dd>open the row under the cursor</dd> |
| 129 | </div> | 164 | </div> | ||
| 130 | <div> | 165 | <div> | ||
| 131 | <dt> | 166 | <dt> | ||
| 132 | - | <kbd>g h</kbd> | 167 | + | <kbd>g g</kbd> |
| 133 | - | <kbd>g t</kbd> | 168 | + | <kbd>G</kbd> |
| 134 | </dt> | 169 | </dt> | ||
| 135 | - | <dd>go home / tree</dd> | 170 | + | <dd>first / last row</dd> |
| 136 | </div> | 171 | </div> | ||
| 137 | <div> | 172 | <div> | ||
| 138 | <dt> | 173 | <dt> | ||
| 139 | - | <kbd>g c</kbd> | 174 | + | <kbd>/</kbd> |
| 140 | - | <kbd>g r</kbd> | |||
| 141 | </dt> | 175 | </dt> | ||
| 142 | - | <dd>go commits / refs</dd> | 176 | + | <dd>filter the list</dd> |
| 143 | </div> | 177 | </div> | ||
| 144 | <div> | 178 | <div> | ||
| 145 | <dt> | 179 | <dt> | ||
| 146 | <kbd>b</kbd> | 180 | <kbd>b</kbd> | ||
| 147 | </dt> | 181 | </dt> | ||
| 148 | - | <dd>switch ref (branches, tags)</dd> | 182 | + | <dd>switch branch or tag</dd> |
| 149 | - | </div> | |||
| 150 | - | <div> | |||
| 151 | - | <dt> | |||
| 152 | - | <kbd>:</kbd> | |||
| 153 | - | <kbd>f</kbd> | |||
| 154 | - | </dt> | |||
| 155 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 156 | - | </div> | |||
| 157 | - | <div> | |||
| 158 | - | <dt> | |||
| 159 | - | <kbd>y</kbd> | |||
| 160 | - | </dt> | |||
| 161 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 162 | </div> | 183 | </div> | ||
| 163 | <div> | 184 | <div> | ||
| 164 | <dt> | 185 | <dt> | ||
| @@ -166,15 +187,21 @@ | |||||
| 166 | <kbd>t</kbd> | 187 | <kbd>t</kbd> | ||
| 167 | <kbd>c</kbd> | 188 | <kbd>c</kbd> | ||
| 168 | <kbd>r</kbd> | 189 | <kbd>r</kbd> | ||
| 190 | + | <kbd>m</kbd> | |||
| 169 | </dt> | 191 | </dt> | ||
| 170 | - | <dd>repo sections (when shown)</dd> | 192 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 171 | </div> | 193 | </div> | ||
| 172 | <div> | 194 | <div> | ||
| 173 | <dt> | 195 | <dt> | ||
| 174 | - | <kbd>m</kbd> | 196 | + | <kbd>g h</kbd> |
| 175 | - | <kbd>g m</kbd> | 197 | + | </dt> |
| 198 | + | <dd>home</dd> | |||
| 199 | + | </div> | |||
| 200 | + | <div> | |||
| 201 | + | <dt> | |||
| 202 | + | <kbd>y</kbd> | |||
| 176 | </dt> | 203 | </dt> | ||
| 177 | - | <dd>merge requests (when shown)</dd> | 204 | + | <dd>copy a permalink pinned to the commit</dd> |
| 178 | </div> | 205 | </div> | ||
| 179 | <div> | 206 | <div> | ||
| 180 | <dt> | 207 | <dt> | ||
| @@ -194,19 +221,21 @@ | |||||
| 194 | <dt> | 221 | <dt> | ||
| 195 | <kbd>esc</kbd> | 222 | <kbd>esc</kbd> | ||
| 196 | </dt> | 223 | </dt> | ||
| 197 | - | <dd>close panel</dd> | 224 | + | <dd>close a dialog, leave a field</dd> |
| 198 | </div> | 225 | </div> | ||
| 199 | </dl> | 226 | </dl> | ||
| 200 | </dialog> | 227 | </dialog> | ||
| 201 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 228 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 202 | <div class="panel-head"> | 229 | <div class="panel-head"> | ||
| 203 | - | <label for="refs-filter">ref></label> | 230 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 204 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 231 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 205 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 232 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 233 | + | <kbd>esc</kbd> | |||
| 234 | + | </button> | |||
| 206 | </div> | 235 | </div> | ||
| 207 | <div class="panel-cols" data-picklist> | 236 | <div class="panel-cols" data-picklist> | ||
| 208 | <section aria-label="Branches"> | 237 | <section aria-label="Branches"> | ||
| 209 | - | <h2>branches <span class="mute">2</span> | 238 | + | <h2>Branches <span class="count">2</span> |
| 210 | </h2> | 239 | </h2> | ||
| 211 | <ul> | 240 | <ul> | ||
| 212 | <li> | 241 | <li> | ||
| @@ -220,7 +249,7 @@ | |||||
| 220 | </ul> | 249 | </ul> | ||
| 221 | </section> | 250 | </section> | ||
| 222 | <section aria-label="Tags"> | 251 | <section aria-label="Tags"> | ||
| 223 | - | <h2>tags <span class="mute">1</span> | 252 | + | <h2>Tags <span class="count">1</span> |
| 224 | </h2> | 253 | </h2> | ||
| 225 | <ul> | 254 | <ul> | ||
| 226 | <li> | 255 | <li> | ||
| @@ -231,105 +260,124 @@ | |||||
| 231 | </section> | 260 | </section> | ||
| 232 | </div> | 261 | </div> | ||
| 233 | </dialog> | 262 | </dialog> | ||
| 234 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 263 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 235 | <div class="panel-head"> | 264 | <div class="panel-head"> | ||
| 236 | - | <label for="cmd-q" class="hi">:</label> | 265 | + | <label for="cmd-q" class="sr">go to</label> |
| 237 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 266 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 238 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 267 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 239 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 268 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 240 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 269 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 241 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 270 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 242 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 271 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 243 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 272 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 244 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 273 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 274 | + | <kbd>esc</kbd> | |||
| 275 | + | </button> | |||
| 245 | </div> | 276 | </div> | ||
| 246 | - | <ul id="cmd-list" class="cmd-list"> | 277 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 247 | <li data-k="go home" data-cursor> | 278 | <li data-k="go home" data-cursor> | ||
| 248 | <a href="/" tabindex="-1"> | 279 | <a href="/" tabindex="-1"> | ||
| 249 | - | <span class="k">go</span>home</a> | 280 | + | <span class="lbl">home</span> |
| 281 | + | <span class="k">go</span> | |||
| 282 | + | </a> | |||
| 250 | </li> | 283 | </li> | ||
| 251 | <li data-k="section summary"> | 284 | <li data-k="section summary"> | ||
| 252 | <a href="/files" tabindex="-1"> | 285 | <a href="/files" tabindex="-1"> | ||
| 253 | - | <span class="k">section</span>summary</a> | 286 | + | <span class="lbl">summary</span> |
| 287 | + | <span class="k">section</span> | |||
| 288 | + | </a> | |||
| 254 | </li> | 289 | </li> | ||
| 255 | - | <li data-k="section tree"> | 290 | + | <li data-k="section files"> |
| 256 | <a href="/files/tree/main" tabindex="-1"> | 291 | <a href="/files/tree/main" tabindex="-1"> | ||
| 257 | - | <span class="k">section</span>tree</a> | 292 | + | <span class="lbl">files</span> |
| 293 | + | <span class="k">section</span> | |||
| 294 | + | </a> | |||
| 258 | </li> | 295 | </li> | ||
| 259 | <li data-k="section commits"> | 296 | <li data-k="section commits"> | ||
| 260 | <a href="/files/commits/main" tabindex="-1"> | 297 | <a href="/files/commits/main" tabindex="-1"> | ||
| 261 | - | <span class="k">section</span>commits</a> | 298 | + | <span class="lbl">commits</span> |
| 299 | + | <span class="k">section</span> | |||
| 300 | + | </a> | |||
| 262 | </li> | 301 | </li> | ||
| 263 | - | <li data-k="section refs"> | 302 | + | <li data-k="section branches & tags"> |
| 264 | <a href="/files/refs" tabindex="-1"> | 303 | <a href="/files/refs" tabindex="-1"> | ||
| 265 | - | <span class="k">section</span>refs</a> | 304 | + | <span class="lbl">branches & tags</span> |
| 305 | + | <span class="k">section</span> | |||
| 306 | + | </a> | |||
| 266 | </li> | 307 | </li> | ||
| 267 | <li data-k="section merge requests"> | 308 | <li data-k="section merge requests"> | ||
| 268 | <a href="/files/merge-requests" tabindex="-1"> | 309 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 269 | - | <span class="k">section</span>merge requests</a> | 310 | + | <span class="lbl">merge requests</span> |
| 311 | + | <span class="k">section</span> | |||
| 312 | + | </a> | |||
| 270 | </li> | 313 | </li> | ||
| 271 | <li data-k="branch feature/x"> | 314 | <li data-k="branch feature/x"> | ||
| 272 | <a href="/files/tree/feature/x" tabindex="-1"> | 315 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 273 | - | <span class="k">branch</span>feature/x</a> | 316 | + | <span class="lbl">feature/x</span> |
| 317 | + | <span class="k">branch</span> | |||
| 318 | + | </a> | |||
| 274 | </li> | 319 | </li> | ||
| 275 | <li data-k="branch main"> | 320 | <li data-k="branch main"> | ||
| 276 | <a href="/files/tree/main" tabindex="-1"> | 321 | <a href="/files/tree/main" tabindex="-1"> | ||
| 277 | - | <span class="k">branch</span>main</a> | 322 | + | <span class="lbl">main</span> |
| 323 | + | <span class="k">branch</span> | |||
| 324 | + | </a> | |||
| 278 | </li> | 325 | </li> | ||
| 279 | <li data-k="tag v1.0"> | 326 | <li data-k="tag v1.0"> | ||
| 280 | <a href="/files/tree/v1.0" tabindex="-1"> | 327 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 281 | - | <span class="k">tag</span>v1.0</a> | 328 | + | <span class="lbl">v1.0</span> |
| 329 | + | <span class="k">tag</span> | |||
| 330 | + | </a> | |||
| 282 | </li> | 331 | </li> | ||
| 283 | </ul> | 332 | </ul> | ||
| 333 | + | <p class="panel-foot"> | |||
| 334 | + | <span> | |||
| 335 | + | <kbd>↑</kbd> | |||
| 336 | + | <kbd>↓</kbd> move</span> | |||
| 337 | + | <span> | |||
| 338 | + | <kbd>enter</kbd> open</span> | |||
| 339 | + | <span> | |||
| 340 | + | <kbd>esc</kbd> close</span> | |||
| 341 | + | </p> | |||
| 284 | </dialog> | 342 | </dialog> | ||
| 285 | - | <footer class="bar status" aria-label="Status"> | 343 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 286 | - | <span class="mode">blob</span> | |||
| 287 | - | <span class="st-ctx">data.bin</span> | |||
| 288 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 289 | - | <span class="hi">:</span> go to…</button> | |||
| 290 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 291 | - | </span> | |||
| 292 | - | <span class="st-hints">click a line number · y permalink · ? help</span> | |||
| 293 | - | <span class="st-src"> | |||
| 294 | - | <span class="st-ver">ermite devel · </span> | |||
| 295 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 296 | - | </span> | |||
| 297 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 298 | - | </footer> | |||
| 299 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 300 | if (q('dialog[open]').count) return; | 344 | if (q('dialog[open]').count) return; | ||
| 301 | let rows = q('[data-rows] > :not([hidden])').arr(); | 345 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 302 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 346 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 303 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 347 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 304 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 348 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 305 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 349 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 350 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 351 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 306 | })(1); | 352 | })(1); | ||
| 307 | - | k-k from:body -> ((d) => { | 353 | + | k-k from:body -> (async (d) => { |
| 308 | if (q('dialog[open]').count) return; | 354 | if (q('dialog[open]').count) return; | ||
| 309 | let rows = q('[data-rows] > :not([hidden])').arr(); | 355 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 310 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 356 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 311 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 357 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 312 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 358 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 313 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 359 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 360 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 361 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 314 | })(-1); | 362 | })(-1); | ||
| 315 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 363 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 316 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 364 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 317 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 365 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 318 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 366 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 319 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 367 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 320 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 368 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 321 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 369 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 322 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 370 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 323 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 371 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 324 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 372 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 325 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 373 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 326 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 374 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 327 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 375 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 328 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 376 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 329 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 377 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 330 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 378 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 331 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 379 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 332 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 380 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 333 | k-next from:body -> { | 381 | k-next from:body -> { | ||
| 334 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 382 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 335 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 383 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/blob-code.html +163 -116
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>main.go · files · example.test</title> | 7 | <title>main.go · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,36 +42,55 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">main</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 68 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 72 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work has-rail"> | 86 | <div class="work has-rail"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <h1 class="file-h"> | 88 | <h1 class="file-h"> | ||
| 66 | <span>main.go</span> | 89 | <span>main.go</span> | ||
| 67 | <span class="acts"> | 90 | <span class="acts"> | ||
| 68 | - | <a href="/files/raw/main/main.go">raw</a> | 91 | + | <a class="btn" href="/files/raw/main/main.go">Raw</a> |
| 69 | - | <a href="/files/commits/main/main.go">history</a> | 92 | + | <a class="btn" href="/files/commits/main/main.go">History</a> |
| 70 | - | <a href="/files/tree/main">dir</a> | 93 | + | <a class="btn" href="/files/tree/main">Folder</a> |
| 71 | </span> | 94 | </span> | ||
| 72 | </h1> | 95 | </h1> | ||
| 73 | <pre class="chroma code" data-anchor="" hx-on:click="let a = event.target.closest('a.ln'); if (!a) return; event.preventDefault(); | 96 | <pre class="chroma code" data-anchor="" hx-on:click="let a = event.target.closest('a.ln'); if (!a) return; event.preventDefault(); | ||
| @@ -136,10 +159,8 @@ | |||||
| 136 | </pre> | 159 | </pre> | ||
| 137 | </main> | 160 | </main> | ||
| 138 | <aside class="rail" aria-label="Context"> | 161 | <aside class="rail" aria-label="Context"> | ||
| 139 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 140 | - | <summary>file · last change · selection</summary> | |||
| 141 | <section aria-labelledby="r-file"> | 162 | <section aria-labelledby="r-file"> | ||
| 142 | - | <h2 id="r-file">file</h2> | 163 | + | <h2 id="r-file">File</h2> |
| 143 | <dl class="kv"> | 164 | <dl class="kv"> | ||
| 144 | <dt>size</dt> | 165 | <dt>size</dt> | ||
| 145 | <dd>87 B</dd> | 166 | <dd>87 B</dd> | ||
| @@ -150,7 +171,7 @@ | |||||
| 150 | </dl> | 171 | </dl> | ||
| 151 | </section> | 172 | </section> | ||
| 152 | <section aria-labelledby="r-last"> | 173 | <section aria-labelledby="r-last"> | ||
| 153 | - | <h2 id="r-last">last change <a href="/files/commits/main/main.go">history</a> | 174 | + | <h2 id="r-last">Last change <a href="/files/commits/main/main.go">History</a> |
| 154 | </h2> | 175 | </h2> | ||
| 155 | <p> | 176 | <p> | ||
| 156 | <a href="/files/commit/SHA40">SHA7</a> add main.go</p> | 177 | <a href="/files/commit/SHA40">SHA7</a> add main.go</p> | ||
| @@ -158,77 +179,77 @@ | |||||
| 158 | </p> | 179 | </p> | ||
| 159 | </section> | 180 | </section> | ||
| 160 | <section aria-labelledby="r-sel"> | 181 | <section aria-labelledby="r-sel"> | ||
| 161 | - | <h2 id="r-sel">selection</h2> | 182 | + | <h2 id="r-sel">Selection</h2> |
| 162 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | 183 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | ||
| 163 | <p class="dim">y copies a permalink pinned to the commit sha</p> | 184 | <p class="dim">y copies a permalink pinned to the commit sha</p> | ||
| 164 | </section> | 185 | </section> | ||
| 165 | - | </details> | |||
| 166 | </aside> | 186 | </aside> | ||
| 167 | </div> | 187 | </div> | ||
| 168 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 188 | + | <footer class="foot"> |
| 189 | + | <span>ermite devel</span> | |||
| 190 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 191 | + | <span class="foot-keys"> | |||
| 192 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 193 | + | </footer> | |||
| 194 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 195 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 196 | + | </span> | |||
| 197 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 198 | + | </span> | |||
| 199 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 200 | + | </span> | |||
| 201 | + | </div> | |||
| 202 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 169 | <div class="panel-head"> | 203 | <div class="panel-head"> | ||
| 170 | - | <span>keys</span> | 204 | + | <h2>Keyboard shortcuts</h2> |
| 171 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 205 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 206 | + | <kbd>esc</kbd> close</button> | |||
| 172 | </div> | 207 | </div> | ||
| 173 | <dl class="keys"> | 208 | <dl class="keys"> | ||
| 174 | <div> | 209 | <div> | ||
| 175 | <dt> | 210 | <dt> | ||
| 176 | - | <kbd>j</kbd> | 211 | + | <kbd>ctrl k</kbd> |
| 177 | - | <kbd>k</kbd> | 212 | + | <kbd>:</kbd> |
| 178 | </dt> | 213 | </dt> | ||
| 179 | - | <dd>move cursor down / up</dd> | 214 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 180 | </div> | 215 | </div> | ||
| 181 | <div> | 216 | <div> | ||
| 182 | <dt> | 217 | <dt> | ||
| 183 | - | <kbd>enter</kbd> | 218 | + | <kbd>?</kbd> |
| 184 | </dt> | 219 | </dt> | ||
| 185 | - | <dd>open row under cursor</dd> | 220 | + | <dd>this list</dd> |
| 186 | </div> | 221 | </div> | ||
| 187 | <div> | 222 | <div> | ||
| 188 | <dt> | 223 | <dt> | ||
| 189 | - | <kbd>g</kbd> | 224 | + | <kbd>j</kbd> |
| 190 | - | <kbd>g</kbd> / <kbd>G</kbd> | 225 | + | <kbd>k</kbd> |
| 191 | </dt> | 226 | </dt> | ||
| 192 | - | <dd>first / last row</dd> | 227 | + | <dd>move the row cursor down / up</dd> |
| 193 | </div> | 228 | </div> | ||
| 194 | <div> | 229 | <div> | ||
| 195 | <dt> | 230 | <dt> | ||
| 196 | - | <kbd>/</kbd> | 231 | + | <kbd>enter</kbd> |
| 197 | </dt> | 232 | </dt> | ||
| 198 | - | <dd>focus the filter</dd> | 233 | + | <dd>open the row under the cursor</dd> |
| 199 | </div> | 234 | </div> | ||
| 200 | <div> | 235 | <div> | ||
| 201 | <dt> | 236 | <dt> | ||
| 202 | - | <kbd>g h</kbd> | 237 | + | <kbd>g g</kbd> |
| 203 | - | <kbd>g t</kbd> | 238 | + | <kbd>G</kbd> |
| 204 | </dt> | 239 | </dt> | ||
| 205 | - | <dd>go home / tree</dd> | 240 | + | <dd>first / last row</dd> |
| 206 | </div> | 241 | </div> | ||
| 207 | <div> | 242 | <div> | ||
| 208 | <dt> | 243 | <dt> | ||
| 209 | - | <kbd>g c</kbd> | 244 | + | <kbd>/</kbd> |
| 210 | - | <kbd>g r</kbd> | |||
| 211 | </dt> | 245 | </dt> | ||
| 212 | - | <dd>go commits / refs</dd> | 246 | + | <dd>filter the list</dd> |
| 213 | </div> | 247 | </div> | ||
| 214 | <div> | 248 | <div> | ||
| 215 | <dt> | 249 | <dt> | ||
| 216 | <kbd>b</kbd> | 250 | <kbd>b</kbd> | ||
| 217 | </dt> | 251 | </dt> | ||
| 218 | - | <dd>switch ref (branches, tags)</dd> | 252 | + | <dd>switch branch or tag</dd> |
| 219 | - | </div> | |||
| 220 | - | <div> | |||
| 221 | - | <dt> | |||
| 222 | - | <kbd>:</kbd> | |||
| 223 | - | <kbd>f</kbd> | |||
| 224 | - | </dt> | |||
| 225 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 226 | - | </div> | |||
| 227 | - | <div> | |||
| 228 | - | <dt> | |||
| 229 | - | <kbd>y</kbd> | |||
| 230 | - | </dt> | |||
| 231 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 232 | </div> | 253 | </div> | ||
| 233 | <div> | 254 | <div> | ||
| 234 | <dt> | 255 | <dt> | ||
| @@ -236,15 +257,21 @@ | |||||
| 236 | <kbd>t</kbd> | 257 | <kbd>t</kbd> | ||
| 237 | <kbd>c</kbd> | 258 | <kbd>c</kbd> | ||
| 238 | <kbd>r</kbd> | 259 | <kbd>r</kbd> | ||
| 260 | + | <kbd>m</kbd> | |||
| 239 | </dt> | 261 | </dt> | ||
| 240 | - | <dd>repo sections (when shown)</dd> | 262 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 241 | </div> | 263 | </div> | ||
| 242 | <div> | 264 | <div> | ||
| 243 | <dt> | 265 | <dt> | ||
| 244 | - | <kbd>m</kbd> | 266 | + | <kbd>g h</kbd> |
| 245 | - | <kbd>g m</kbd> | 267 | + | </dt> |
| 268 | + | <dd>home</dd> | |||
| 269 | + | </div> | |||
| 270 | + | <div> | |||
| 271 | + | <dt> | |||
| 272 | + | <kbd>y</kbd> | |||
| 246 | </dt> | 273 | </dt> | ||
| 247 | - | <dd>merge requests (when shown)</dd> | 274 | + | <dd>copy a permalink pinned to the commit</dd> |
| 248 | </div> | 275 | </div> | ||
| 249 | <div> | 276 | <div> | ||
| 250 | <dt> | 277 | <dt> | ||
| @@ -264,19 +291,21 @@ | |||||
| 264 | <dt> | 291 | <dt> | ||
| 265 | <kbd>esc</kbd> | 292 | <kbd>esc</kbd> | ||
| 266 | </dt> | 293 | </dt> | ||
| 267 | - | <dd>close panel</dd> | 294 | + | <dd>close a dialog, leave a field</dd> |
| 268 | </div> | 295 | </div> | ||
| 269 | </dl> | 296 | </dl> | ||
| 270 | </dialog> | 297 | </dialog> | ||
| 271 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 298 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 272 | <div class="panel-head"> | 299 | <div class="panel-head"> | ||
| 273 | - | <label for="refs-filter">ref></label> | 300 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 274 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 301 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 275 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 302 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 303 | + | <kbd>esc</kbd> | |||
| 304 | + | </button> | |||
| 276 | </div> | 305 | </div> | ||
| 277 | <div class="panel-cols" data-picklist> | 306 | <div class="panel-cols" data-picklist> | ||
| 278 | <section aria-label="Branches"> | 307 | <section aria-label="Branches"> | ||
| 279 | - | <h2>branches <span class="mute">2</span> | 308 | + | <h2>Branches <span class="count">2</span> |
| 280 | </h2> | 309 | </h2> | ||
| 281 | <ul> | 310 | <ul> | ||
| 282 | <li> | 311 | <li> | ||
| @@ -290,7 +319,7 @@ | |||||
| 290 | </ul> | 319 | </ul> | ||
| 291 | </section> | 320 | </section> | ||
| 292 | <section aria-label="Tags"> | 321 | <section aria-label="Tags"> | ||
| 293 | - | <h2>tags <span class="mute">1</span> | 322 | + | <h2>Tags <span class="count">1</span> |
| 294 | </h2> | 323 | </h2> | ||
| 295 | <ul> | 324 | <ul> | ||
| 296 | <li> | 325 | <li> | ||
| @@ -301,106 +330,124 @@ | |||||
| 301 | </section> | 330 | </section> | ||
| 302 | </div> | 331 | </div> | ||
| 303 | </dialog> | 332 | </dialog> | ||
| 304 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 333 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 305 | <div class="panel-head"> | 334 | <div class="panel-head"> | ||
| 306 | - | <label for="cmd-q" class="hi">:</label> | 335 | + | <label for="cmd-q" class="sr">go to</label> |
| 307 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 336 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 308 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 337 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 309 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 338 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 310 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 339 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 311 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 340 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 312 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 341 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 313 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 342 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 314 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 343 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 344 | + | <kbd>esc</kbd> | |||
| 345 | + | </button> | |||
| 315 | </div> | 346 | </div> | ||
| 316 | - | <ul id="cmd-list" class="cmd-list"> | 347 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 317 | <li data-k="go home" data-cursor> | 348 | <li data-k="go home" data-cursor> | ||
| 318 | <a href="/" tabindex="-1"> | 349 | <a href="/" tabindex="-1"> | ||
| 319 | - | <span class="k">go</span>home</a> | 350 | + | <span class="lbl">home</span> |
| 351 | + | <span class="k">go</span> | |||
| 352 | + | </a> | |||
| 320 | </li> | 353 | </li> | ||
| 321 | <li data-k="section summary"> | 354 | <li data-k="section summary"> | ||
| 322 | <a href="/files" tabindex="-1"> | 355 | <a href="/files" tabindex="-1"> | ||
| 323 | - | <span class="k">section</span>summary</a> | 356 | + | <span class="lbl">summary</span> |
| 357 | + | <span class="k">section</span> | |||
| 358 | + | </a> | |||
| 324 | </li> | 359 | </li> | ||
| 325 | - | <li data-k="section tree"> | 360 | + | <li data-k="section files"> |
| 326 | <a href="/files/tree/main" tabindex="-1"> | 361 | <a href="/files/tree/main" tabindex="-1"> | ||
| 327 | - | <span class="k">section</span>tree</a> | 362 | + | <span class="lbl">files</span> |
| 363 | + | <span class="k">section</span> | |||
| 364 | + | </a> | |||
| 328 | </li> | 365 | </li> | ||
| 329 | <li data-k="section commits"> | 366 | <li data-k="section commits"> | ||
| 330 | <a href="/files/commits/main" tabindex="-1"> | 367 | <a href="/files/commits/main" tabindex="-1"> | ||
| 331 | - | <span class="k">section</span>commits</a> | 368 | + | <span class="lbl">commits</span> |
| 369 | + | <span class="k">section</span> | |||
| 370 | + | </a> | |||
| 332 | </li> | 371 | </li> | ||
| 333 | - | <li data-k="section refs"> | 372 | + | <li data-k="section branches & tags"> |
| 334 | <a href="/files/refs" tabindex="-1"> | 373 | <a href="/files/refs" tabindex="-1"> | ||
| 335 | - | <span class="k">section</span>refs</a> | 374 | + | <span class="lbl">branches & tags</span> |
| 375 | + | <span class="k">section</span> | |||
| 376 | + | </a> | |||
| 336 | </li> | 377 | </li> | ||
| 337 | <li data-k="section merge requests"> | 378 | <li data-k="section merge requests"> | ||
| 338 | <a href="/files/merge-requests" tabindex="-1"> | 379 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 339 | - | <span class="k">section</span>merge requests</a> | 380 | + | <span class="lbl">merge requests</span> |
| 381 | + | <span class="k">section</span> | |||
| 382 | + | </a> | |||
| 340 | </li> | 383 | </li> | ||
| 341 | <li data-k="branch feature/x"> | 384 | <li data-k="branch feature/x"> | ||
| 342 | <a href="/files/tree/feature/x" tabindex="-1"> | 385 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 343 | - | <span class="k">branch</span>feature/x</a> | 386 | + | <span class="lbl">feature/x</span> |
| 387 | + | <span class="k">branch</span> | |||
| 388 | + | </a> | |||
| 344 | </li> | 389 | </li> | ||
| 345 | <li data-k="branch main"> | 390 | <li data-k="branch main"> | ||
| 346 | <a href="/files/tree/main" tabindex="-1"> | 391 | <a href="/files/tree/main" tabindex="-1"> | ||
| 347 | - | <span class="k">branch</span>main</a> | 392 | + | <span class="lbl">main</span> |
| 393 | + | <span class="k">branch</span> | |||
| 394 | + | </a> | |||
| 348 | </li> | 395 | </li> | ||
| 349 | <li data-k="tag v1.0"> | 396 | <li data-k="tag v1.0"> | ||
| 350 | <a href="/files/tree/v1.0" tabindex="-1"> | 397 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 351 | - | <span class="k">tag</span>v1.0</a> | 398 | + | <span class="lbl">v1.0</span> |
| 399 | + | <span class="k">tag</span> | |||
| 400 | + | </a> | |||
| 352 | </li> | 401 | </li> | ||
| 353 | </ul> | 402 | </ul> | ||
| 403 | + | <p class="panel-foot"> | |||
| 404 | + | <span> | |||
| 405 | + | <kbd>↑</kbd> | |||
| 406 | + | <kbd>↓</kbd> move</span> | |||
| 407 | + | <span> | |||
| 408 | + | <kbd>enter</kbd> open</span> | |||
| 409 | + | <span> | |||
| 410 | + | <kbd>esc</kbd> close</span> | |||
| 411 | + | </p> | |||
| 354 | </dialog> | 412 | </dialog> | ||
| 355 | - | <footer class="bar status" aria-label="Status"> | 413 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 356 | - | <span class="mode">blob</span> | |||
| 357 | - | <span class="st-ctx">main.go</span> | |||
| 358 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 359 | - | <span class="hi">:</span> go to…</button> | |||
| 360 | - | <span class="st-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : 'click a line number · shift-click extends'">click a line number · shift-click extends</span> | |||
| 361 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 362 | - | </span> | |||
| 363 | - | <span class="st-hints">click a line number · y permalink · ? help</span> | |||
| 364 | - | <span class="st-src"> | |||
| 365 | - | <span class="st-ver">ermite devel · </span> | |||
| 366 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 367 | - | </span> | |||
| 368 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 369 | - | </footer> | |||
| 370 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 371 | if (q('dialog[open]').count) return; | 414 | if (q('dialog[open]').count) return; | ||
| 372 | let rows = q('[data-rows] > :not([hidden])').arr(); | 415 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 373 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 416 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 374 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 417 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 375 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 418 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 376 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 419 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 420 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 421 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 377 | })(1); | 422 | })(1); | ||
| 378 | - | k-k from:body -> ((d) => { | 423 | + | k-k from:body -> (async (d) => { |
| 379 | if (q('dialog[open]').count) return; | 424 | if (q('dialog[open]').count) return; | ||
| 380 | let rows = q('[data-rows] > :not([hidden])').arr(); | 425 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 381 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 426 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 382 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 427 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 383 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 428 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 384 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 429 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 430 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 431 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 385 | })(-1); | 432 | })(-1); | ||
| 386 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 433 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 387 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 434 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 388 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 435 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 389 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 436 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 390 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 437 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 391 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 438 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 392 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 439 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 393 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 440 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 394 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 441 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 395 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 442 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 396 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 443 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 397 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 444 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 398 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 445 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 399 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 446 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 400 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 447 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 401 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 448 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 402 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 449 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 403 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 450 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 404 | k-next from:body -> { | 451 | k-next from:body -> { | ||
| 405 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 452 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 406 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 453 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/blob-empty.html +162 -114
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>empty.txt · files · example.test</title> | 7 | <title>empty.txt · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,45 +42,62 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">main</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 68 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 72 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work has-rail"> | 86 | <div class="work has-rail"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <h1 class="file-h"> | 88 | <h1 class="file-h"> | ||
| 66 | <span>empty.txt</span> | 89 | <span>empty.txt</span> | ||
| 67 | <span class="acts"> | 90 | <span class="acts"> | ||
| 68 | - | <a href="/files/raw/main/empty.txt">raw</a> | 91 | + | <a class="btn" href="/files/raw/main/empty.txt">Raw</a> |
| 69 | - | <a href="/files/commits/main/empty.txt">history</a> | 92 | + | <a class="btn" href="/files/commits/main/empty.txt">History</a> |
| 70 | - | <a href="/files/tree/main">dir</a> | 93 | + | <a class="btn" href="/files/tree/main">Folder</a> |
| 71 | </span> | 94 | </span> | ||
| 72 | </h1> | 95 | </h1> | ||
| 73 | <p class="notice" role="note">empty file.</p> | 96 | <p class="notice" role="note">empty file.</p> | ||
| 74 | </main> | 97 | </main> | ||
| 75 | <aside class="rail" aria-label="Context"> | 98 | <aside class="rail" aria-label="Context"> | ||
| 76 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 77 | - | <summary>file · last change · selection</summary> | |||
| 78 | <section aria-labelledby="r-file"> | 99 | <section aria-labelledby="r-file"> | ||
| 79 | - | <h2 id="r-file">file</h2> | 100 | + | <h2 id="r-file">File</h2> |
| 80 | <dl class="kv"> | 101 | <dl class="kv"> | ||
| 81 | <dt>size</dt> | 102 | <dt>size</dt> | ||
| 82 | <dd>0 B</dd> | 103 | <dd>0 B</dd> | ||
| @@ -85,80 +106,80 @@ | |||||
| 85 | </dl> | 106 | </dl> | ||
| 86 | </section> | 107 | </section> | ||
| 87 | <section aria-labelledby="r-last"> | 108 | <section aria-labelledby="r-last"> | ||
| 88 | - | <h2 id="r-last">last change <a href="/files/commits/main/empty.txt">history</a> | 109 | + | <h2 id="r-last">Last change <a href="/files/commits/main/empty.txt">History</a> |
| 89 | </h2> | 110 | </h2> | ||
| 90 | <p> | 111 | <p> | ||
| 91 | <a href="/files/commit/SHA40">SHA7</a> add empty.txt</p> | 112 | <a href="/files/commit/SHA40">SHA7</a> add empty.txt</p> | ||
| 92 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | 113 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 93 | </p> | 114 | </p> | ||
| 94 | </section> | 115 | </section> | ||
| 95 | - | </details> | |||
| 96 | </aside> | 116 | </aside> | ||
| 97 | </div> | 117 | </div> | ||
| 98 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 118 | + | <footer class="foot"> |
| 119 | + | <span>ermite devel</span> | |||
| 120 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 121 | + | <span class="foot-keys"> | |||
| 122 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 123 | + | </footer> | |||
| 124 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 125 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 126 | + | </span> | |||
| 127 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 128 | + | </span> | |||
| 129 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 130 | + | </span> | |||
| 131 | + | </div> | |||
| 132 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 99 | <div class="panel-head"> | 133 | <div class="panel-head"> | ||
| 100 | - | <span>keys</span> | 134 | + | <h2>Keyboard shortcuts</h2> |
| 101 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 135 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 136 | + | <kbd>esc</kbd> close</button> | |||
| 102 | </div> | 137 | </div> | ||
| 103 | <dl class="keys"> | 138 | <dl class="keys"> | ||
| 104 | <div> | 139 | <div> | ||
| 105 | <dt> | 140 | <dt> | ||
| 106 | - | <kbd>j</kbd> | 141 | + | <kbd>ctrl k</kbd> |
| 107 | - | <kbd>k</kbd> | 142 | + | <kbd>:</kbd> |
| 108 | </dt> | 143 | </dt> | ||
| 109 | - | <dd>move cursor down / up</dd> | 144 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 110 | </div> | 145 | </div> | ||
| 111 | <div> | 146 | <div> | ||
| 112 | <dt> | 147 | <dt> | ||
| 113 | - | <kbd>enter</kbd> | 148 | + | <kbd>?</kbd> |
| 114 | </dt> | 149 | </dt> | ||
| 115 | - | <dd>open row under cursor</dd> | 150 | + | <dd>this list</dd> |
| 116 | </div> | 151 | </div> | ||
| 117 | <div> | 152 | <div> | ||
| 118 | <dt> | 153 | <dt> | ||
| 119 | - | <kbd>g</kbd> | 154 | + | <kbd>j</kbd> |
| 120 | - | <kbd>g</kbd> / <kbd>G</kbd> | 155 | + | <kbd>k</kbd> |
| 121 | </dt> | 156 | </dt> | ||
| 122 | - | <dd>first / last row</dd> | 157 | + | <dd>move the row cursor down / up</dd> |
| 123 | </div> | 158 | </div> | ||
| 124 | <div> | 159 | <div> | ||
| 125 | <dt> | 160 | <dt> | ||
| 126 | - | <kbd>/</kbd> | 161 | + | <kbd>enter</kbd> |
| 127 | </dt> | 162 | </dt> | ||
| 128 | - | <dd>focus the filter</dd> | 163 | + | <dd>open the row under the cursor</dd> |
| 129 | </div> | 164 | </div> | ||
| 130 | <div> | 165 | <div> | ||
| 131 | <dt> | 166 | <dt> | ||
| 132 | - | <kbd>g h</kbd> | 167 | + | <kbd>g g</kbd> |
| 133 | - | <kbd>g t</kbd> | 168 | + | <kbd>G</kbd> |
| 134 | </dt> | 169 | </dt> | ||
| 135 | - | <dd>go home / tree</dd> | 170 | + | <dd>first / last row</dd> |
| 136 | </div> | 171 | </div> | ||
| 137 | <div> | 172 | <div> | ||
| 138 | <dt> | 173 | <dt> | ||
| 139 | - | <kbd>g c</kbd> | 174 | + | <kbd>/</kbd> |
| 140 | - | <kbd>g r</kbd> | |||
| 141 | </dt> | 175 | </dt> | ||
| 142 | - | <dd>go commits / refs</dd> | 176 | + | <dd>filter the list</dd> |
| 143 | </div> | 177 | </div> | ||
| 144 | <div> | 178 | <div> | ||
| 145 | <dt> | 179 | <dt> | ||
| 146 | <kbd>b</kbd> | 180 | <kbd>b</kbd> | ||
| 147 | </dt> | 181 | </dt> | ||
| 148 | - | <dd>switch ref (branches, tags)</dd> | 182 | + | <dd>switch branch or tag</dd> |
| 149 | - | </div> | |||
| 150 | - | <div> | |||
| 151 | - | <dt> | |||
| 152 | - | <kbd>:</kbd> | |||
| 153 | - | <kbd>f</kbd> | |||
| 154 | - | </dt> | |||
| 155 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 156 | - | </div> | |||
| 157 | - | <div> | |||
| 158 | - | <dt> | |||
| 159 | - | <kbd>y</kbd> | |||
| 160 | - | </dt> | |||
| 161 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 162 | </div> | 183 | </div> | ||
| 163 | <div> | 184 | <div> | ||
| 164 | <dt> | 185 | <dt> | ||
| @@ -166,15 +187,21 @@ | |||||
| 166 | <kbd>t</kbd> | 187 | <kbd>t</kbd> | ||
| 167 | <kbd>c</kbd> | 188 | <kbd>c</kbd> | ||
| 168 | <kbd>r</kbd> | 189 | <kbd>r</kbd> | ||
| 190 | + | <kbd>m</kbd> | |||
| 169 | </dt> | 191 | </dt> | ||
| 170 | - | <dd>repo sections (when shown)</dd> | 192 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 171 | </div> | 193 | </div> | ||
| 172 | <div> | 194 | <div> | ||
| 173 | <dt> | 195 | <dt> | ||
| 174 | - | <kbd>m</kbd> | 196 | + | <kbd>g h</kbd> |
| 175 | - | <kbd>g m</kbd> | 197 | + | </dt> |
| 198 | + | <dd>home</dd> | |||
| 199 | + | </div> | |||
| 200 | + | <div> | |||
| 201 | + | <dt> | |||
| 202 | + | <kbd>y</kbd> | |||
| 176 | </dt> | 203 | </dt> | ||
| 177 | - | <dd>merge requests (when shown)</dd> | 204 | + | <dd>copy a permalink pinned to the commit</dd> |
| 178 | </div> | 205 | </div> | ||
| 179 | <div> | 206 | <div> | ||
| 180 | <dt> | 207 | <dt> | ||
| @@ -194,19 +221,21 @@ | |||||
| 194 | <dt> | 221 | <dt> | ||
| 195 | <kbd>esc</kbd> | 222 | <kbd>esc</kbd> | ||
| 196 | </dt> | 223 | </dt> | ||
| 197 | - | <dd>close panel</dd> | 224 | + | <dd>close a dialog, leave a field</dd> |
| 198 | </div> | 225 | </div> | ||
| 199 | </dl> | 226 | </dl> | ||
| 200 | </dialog> | 227 | </dialog> | ||
| 201 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 228 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 202 | <div class="panel-head"> | 229 | <div class="panel-head"> | ||
| 203 | - | <label for="refs-filter">ref></label> | 230 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 204 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 231 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 205 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 232 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 233 | + | <kbd>esc</kbd> | |||
| 234 | + | </button> | |||
| 206 | </div> | 235 | </div> | ||
| 207 | <div class="panel-cols" data-picklist> | 236 | <div class="panel-cols" data-picklist> | ||
| 208 | <section aria-label="Branches"> | 237 | <section aria-label="Branches"> | ||
| 209 | - | <h2>branches <span class="mute">2</span> | 238 | + | <h2>Branches <span class="count">2</span> |
| 210 | </h2> | 239 | </h2> | ||
| 211 | <ul> | 240 | <ul> | ||
| 212 | <li> | 241 | <li> | ||
| @@ -220,7 +249,7 @@ | |||||
| 220 | </ul> | 249 | </ul> | ||
| 221 | </section> | 250 | </section> | ||
| 222 | <section aria-label="Tags"> | 251 | <section aria-label="Tags"> | ||
| 223 | - | <h2>tags <span class="mute">1</span> | 252 | + | <h2>Tags <span class="count">1</span> |
| 224 | </h2> | 253 | </h2> | ||
| 225 | <ul> | 254 | <ul> | ||
| 226 | <li> | 255 | <li> | ||
| @@ -231,105 +260,124 @@ | |||||
| 231 | </section> | 260 | </section> | ||
| 232 | </div> | 261 | </div> | ||
| 233 | </dialog> | 262 | </dialog> | ||
| 234 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 263 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 235 | <div class="panel-head"> | 264 | <div class="panel-head"> | ||
| 236 | - | <label for="cmd-q" class="hi">:</label> | 265 | + | <label for="cmd-q" class="sr">go to</label> |
| 237 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 266 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 238 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 267 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 239 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 268 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 240 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 269 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 241 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 270 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 242 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 271 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 243 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 272 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 244 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 273 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 274 | + | <kbd>esc</kbd> | |||
| 275 | + | </button> | |||
| 245 | </div> | 276 | </div> | ||
| 246 | - | <ul id="cmd-list" class="cmd-list"> | 277 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 247 | <li data-k="go home" data-cursor> | 278 | <li data-k="go home" data-cursor> | ||
| 248 | <a href="/" tabindex="-1"> | 279 | <a href="/" tabindex="-1"> | ||
| 249 | - | <span class="k">go</span>home</a> | 280 | + | <span class="lbl">home</span> |
| 281 | + | <span class="k">go</span> | |||
| 282 | + | </a> | |||
| 250 | </li> | 283 | </li> | ||
| 251 | <li data-k="section summary"> | 284 | <li data-k="section summary"> | ||
| 252 | <a href="/files" tabindex="-1"> | 285 | <a href="/files" tabindex="-1"> | ||
| 253 | - | <span class="k">section</span>summary</a> | 286 | + | <span class="lbl">summary</span> |
| 287 | + | <span class="k">section</span> | |||
| 288 | + | </a> | |||
| 254 | </li> | 289 | </li> | ||
| 255 | - | <li data-k="section tree"> | 290 | + | <li data-k="section files"> |
| 256 | <a href="/files/tree/main" tabindex="-1"> | 291 | <a href="/files/tree/main" tabindex="-1"> | ||
| 257 | - | <span class="k">section</span>tree</a> | 292 | + | <span class="lbl">files</span> |
| 293 | + | <span class="k">section</span> | |||
| 294 | + | </a> | |||
| 258 | </li> | 295 | </li> | ||
| 259 | <li data-k="section commits"> | 296 | <li data-k="section commits"> | ||
| 260 | <a href="/files/commits/main" tabindex="-1"> | 297 | <a href="/files/commits/main" tabindex="-1"> | ||
| 261 | - | <span class="k">section</span>commits</a> | 298 | + | <span class="lbl">commits</span> |
| 299 | + | <span class="k">section</span> | |||
| 300 | + | </a> | |||
| 262 | </li> | 301 | </li> | ||
| 263 | - | <li data-k="section refs"> | 302 | + | <li data-k="section branches & tags"> |
| 264 | <a href="/files/refs" tabindex="-1"> | 303 | <a href="/files/refs" tabindex="-1"> | ||
| 265 | - | <span class="k">section</span>refs</a> | 304 | + | <span class="lbl">branches & tags</span> |
| 305 | + | <span class="k">section</span> | |||
| 306 | + | </a> | |||
| 266 | </li> | 307 | </li> | ||
| 267 | <li data-k="section merge requests"> | 308 | <li data-k="section merge requests"> | ||
| 268 | <a href="/files/merge-requests" tabindex="-1"> | 309 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 269 | - | <span class="k">section</span>merge requests</a> | 310 | + | <span class="lbl">merge requests</span> |
| 311 | + | <span class="k">section</span> | |||
| 312 | + | </a> | |||
| 270 | </li> | 313 | </li> | ||
| 271 | <li data-k="branch feature/x"> | 314 | <li data-k="branch feature/x"> | ||
| 272 | <a href="/files/tree/feature/x" tabindex="-1"> | 315 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 273 | - | <span class="k">branch</span>feature/x</a> | 316 | + | <span class="lbl">feature/x</span> |
| 317 | + | <span class="k">branch</span> | |||
| 318 | + | </a> | |||
| 274 | </li> | 319 | </li> | ||
| 275 | <li data-k="branch main"> | 320 | <li data-k="branch main"> | ||
| 276 | <a href="/files/tree/main" tabindex="-1"> | 321 | <a href="/files/tree/main" tabindex="-1"> | ||
| 277 | - | <span class="k">branch</span>main</a> | 322 | + | <span class="lbl">main</span> |
| 323 | + | <span class="k">branch</span> | |||
| 324 | + | </a> | |||
| 278 | </li> | 325 | </li> | ||
| 279 | <li data-k="tag v1.0"> | 326 | <li data-k="tag v1.0"> | ||
| 280 | <a href="/files/tree/v1.0" tabindex="-1"> | 327 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 281 | - | <span class="k">tag</span>v1.0</a> | 328 | + | <span class="lbl">v1.0</span> |
| 329 | + | <span class="k">tag</span> | |||
| 330 | + | </a> | |||
| 282 | </li> | 331 | </li> | ||
| 283 | </ul> | 332 | </ul> | ||
| 333 | + | <p class="panel-foot"> | |||
| 334 | + | <span> | |||
| 335 | + | <kbd>↑</kbd> | |||
| 336 | + | <kbd>↓</kbd> move</span> | |||
| 337 | + | <span> | |||
| 338 | + | <kbd>enter</kbd> open</span> | |||
| 339 | + | <span> | |||
| 340 | + | <kbd>esc</kbd> close</span> | |||
| 341 | + | </p> | |||
| 284 | </dialog> | 342 | </dialog> | ||
| 285 | - | <footer class="bar status" aria-label="Status"> | 343 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 286 | - | <span class="mode">blob</span> | |||
| 287 | - | <span class="st-ctx">empty.txt</span> | |||
| 288 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 289 | - | <span class="hi">:</span> go to…</button> | |||
| 290 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 291 | - | </span> | |||
| 292 | - | <span class="st-hints">click a line number · y permalink · ? help</span> | |||
| 293 | - | <span class="st-src"> | |||
| 294 | - | <span class="st-ver">ermite devel · </span> | |||
| 295 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 296 | - | </span> | |||
| 297 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 298 | - | </footer> | |||
| 299 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 300 | if (q('dialog[open]').count) return; | 344 | if (q('dialog[open]').count) return; | ||
| 301 | let rows = q('[data-rows] > :not([hidden])').arr(); | 345 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 302 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 346 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 303 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 347 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 304 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 348 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 305 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 349 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 350 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 351 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 306 | })(1); | 352 | })(1); | ||
| 307 | - | k-k from:body -> ((d) => { | 353 | + | k-k from:body -> (async (d) => { |
| 308 | if (q('dialog[open]').count) return; | 354 | if (q('dialog[open]').count) return; | ||
| 309 | let rows = q('[data-rows] > :not([hidden])').arr(); | 355 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 310 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 356 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 311 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 357 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 312 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 358 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 313 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 359 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 360 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 361 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 314 | })(-1); | 362 | })(-1); | ||
| 315 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 363 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 316 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 364 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 317 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 365 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 318 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 366 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 319 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 367 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 320 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 368 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 321 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 369 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 322 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 370 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 323 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 371 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 324 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 372 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 325 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 373 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 326 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 374 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 327 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 375 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 328 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 376 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 329 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 377 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 330 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 378 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 331 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 379 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 332 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 380 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 333 | k-next from:body -> { | 381 | k-next from:body -> { | ||
| 334 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 382 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 335 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 383 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/blob-image.html +162 -114
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>pic.png · files · example.test</title> | 7 | <title>pic.png · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,36 +42,55 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">main</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 68 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 72 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work has-rail"> | 86 | <div class="work has-rail"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <h1 class="file-h"> | 88 | <h1 class="file-h"> | ||
| 66 | <span>pic.png</span> | 89 | <span>pic.png</span> | ||
| 67 | <span class="acts"> | 90 | <span class="acts"> | ||
| 68 | - | <a href="/files/raw/main/pic.png">raw</a> | 91 | + | <a class="btn" href="/files/raw/main/pic.png">Raw</a> |
| 69 | - | <a href="/files/commits/main/pic.png">history</a> | 92 | + | <a class="btn" href="/files/commits/main/pic.png">History</a> |
| 70 | - | <a href="/files/tree/main">dir</a> | 93 | + | <a class="btn" href="/files/tree/main">Folder</a> |
| 71 | </span> | 94 | </span> | ||
| 72 | </h1> | 95 | </h1> | ||
| 73 | <p class="img-view"> | 96 | <p class="img-view"> | ||
| @@ -75,10 +98,8 @@ | |||||
| 75 | </p> | 98 | </p> | ||
| 76 | </main> | 99 | </main> | ||
| 77 | <aside class="rail" aria-label="Context"> | 100 | <aside class="rail" aria-label="Context"> | ||
| 78 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 79 | - | <summary>file · last change · selection</summary> | |||
| 80 | <section aria-labelledby="r-file"> | 101 | <section aria-labelledby="r-file"> | ||
| 81 | - | <h2 id="r-file">file</h2> | 102 | + | <h2 id="r-file">File</h2> |
| 82 | <dl class="kv"> | 103 | <dl class="kv"> | ||
| 83 | <dt>size</dt> | 104 | <dt>size</dt> | ||
| 84 | <dd>88 B</dd> | 105 | <dd>88 B</dd> | ||
| @@ -87,80 +108,80 @@ | |||||
| 87 | </dl> | 108 | </dl> | ||
| 88 | </section> | 109 | </section> | ||
| 89 | <section aria-labelledby="r-last"> | 110 | <section aria-labelledby="r-last"> | ||
| 90 | - | <h2 id="r-last">last change <a href="/files/commits/main/pic.png">history</a> | 111 | + | <h2 id="r-last">Last change <a href="/files/commits/main/pic.png">History</a> |
| 91 | </h2> | 112 | </h2> | ||
| 92 | <p> | 113 | <p> | ||
| 93 | <a href="/files/commit/SHA40">SHA7</a> add pic.png</p> | 114 | <a href="/files/commit/SHA40">SHA7</a> add pic.png</p> | ||
| 94 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | 115 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 95 | </p> | 116 | </p> | ||
| 96 | </section> | 117 | </section> | ||
| 97 | - | </details> | |||
| 98 | </aside> | 118 | </aside> | ||
| 99 | </div> | 119 | </div> | ||
| 100 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 120 | + | <footer class="foot"> |
| 121 | + | <span>ermite devel</span> | |||
| 122 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 123 | + | <span class="foot-keys"> | |||
| 124 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 125 | + | </footer> | |||
| 126 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 127 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 128 | + | </span> | |||
| 129 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 130 | + | </span> | |||
| 131 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 132 | + | </span> | |||
| 133 | + | </div> | |||
| 134 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 101 | <div class="panel-head"> | 135 | <div class="panel-head"> | ||
| 102 | - | <span>keys</span> | 136 | + | <h2>Keyboard shortcuts</h2> |
| 103 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 137 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 138 | + | <kbd>esc</kbd> close</button> | |||
| 104 | </div> | 139 | </div> | ||
| 105 | <dl class="keys"> | 140 | <dl class="keys"> | ||
| 106 | <div> | 141 | <div> | ||
| 107 | <dt> | 142 | <dt> | ||
| 108 | - | <kbd>j</kbd> | 143 | + | <kbd>ctrl k</kbd> |
| 109 | - | <kbd>k</kbd> | 144 | + | <kbd>:</kbd> |
| 110 | </dt> | 145 | </dt> | ||
| 111 | - | <dd>move cursor down / up</dd> | 146 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 112 | </div> | 147 | </div> | ||
| 113 | <div> | 148 | <div> | ||
| 114 | <dt> | 149 | <dt> | ||
| 115 | - | <kbd>enter</kbd> | 150 | + | <kbd>?</kbd> |
| 116 | </dt> | 151 | </dt> | ||
| 117 | - | <dd>open row under cursor</dd> | 152 | + | <dd>this list</dd> |
| 118 | </div> | 153 | </div> | ||
| 119 | <div> | 154 | <div> | ||
| 120 | <dt> | 155 | <dt> | ||
| 121 | - | <kbd>g</kbd> | 156 | + | <kbd>j</kbd> |
| 122 | - | <kbd>g</kbd> / <kbd>G</kbd> | 157 | + | <kbd>k</kbd> |
| 123 | </dt> | 158 | </dt> | ||
| 124 | - | <dd>first / last row</dd> | 159 | + | <dd>move the row cursor down / up</dd> |
| 125 | </div> | 160 | </div> | ||
| 126 | <div> | 161 | <div> | ||
| 127 | <dt> | 162 | <dt> | ||
| 128 | - | <kbd>/</kbd> | 163 | + | <kbd>enter</kbd> |
| 129 | </dt> | 164 | </dt> | ||
| 130 | - | <dd>focus the filter</dd> | 165 | + | <dd>open the row under the cursor</dd> |
| 131 | </div> | 166 | </div> | ||
| 132 | <div> | 167 | <div> | ||
| 133 | <dt> | 168 | <dt> | ||
| 134 | - | <kbd>g h</kbd> | 169 | + | <kbd>g g</kbd> |
| 135 | - | <kbd>g t</kbd> | 170 | + | <kbd>G</kbd> |
| 136 | </dt> | 171 | </dt> | ||
| 137 | - | <dd>go home / tree</dd> | 172 | + | <dd>first / last row</dd> |
| 138 | </div> | 173 | </div> | ||
| 139 | <div> | 174 | <div> | ||
| 140 | <dt> | 175 | <dt> | ||
| 141 | - | <kbd>g c</kbd> | 176 | + | <kbd>/</kbd> |
| 142 | - | <kbd>g r</kbd> | |||
| 143 | </dt> | 177 | </dt> | ||
| 144 | - | <dd>go commits / refs</dd> | 178 | + | <dd>filter the list</dd> |
| 145 | </div> | 179 | </div> | ||
| 146 | <div> | 180 | <div> | ||
| 147 | <dt> | 181 | <dt> | ||
| 148 | <kbd>b</kbd> | 182 | <kbd>b</kbd> | ||
| 149 | </dt> | 183 | </dt> | ||
| 150 | - | <dd>switch ref (branches, tags)</dd> | 184 | + | <dd>switch branch or tag</dd> |
| 151 | - | </div> | |||
| 152 | - | <div> | |||
| 153 | - | <dt> | |||
| 154 | - | <kbd>:</kbd> | |||
| 155 | - | <kbd>f</kbd> | |||
| 156 | - | </dt> | |||
| 157 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 158 | - | </div> | |||
| 159 | - | <div> | |||
| 160 | - | <dt> | |||
| 161 | - | <kbd>y</kbd> | |||
| 162 | - | </dt> | |||
| 163 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 164 | </div> | 185 | </div> | ||
| 165 | <div> | 186 | <div> | ||
| 166 | <dt> | 187 | <dt> | ||
| @@ -168,15 +189,21 @@ | |||||
| 168 | <kbd>t</kbd> | 189 | <kbd>t</kbd> | ||
| 169 | <kbd>c</kbd> | 190 | <kbd>c</kbd> | ||
| 170 | <kbd>r</kbd> | 191 | <kbd>r</kbd> | ||
| 192 | + | <kbd>m</kbd> | |||
| 171 | </dt> | 193 | </dt> | ||
| 172 | - | <dd>repo sections (when shown)</dd> | 194 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 173 | </div> | 195 | </div> | ||
| 174 | <div> | 196 | <div> | ||
| 175 | <dt> | 197 | <dt> | ||
| 176 | - | <kbd>m</kbd> | 198 | + | <kbd>g h</kbd> |
| 177 | - | <kbd>g m</kbd> | 199 | + | </dt> |
| 200 | + | <dd>home</dd> | |||
| 201 | + | </div> | |||
| 202 | + | <div> | |||
| 203 | + | <dt> | |||
| 204 | + | <kbd>y</kbd> | |||
| 178 | </dt> | 205 | </dt> | ||
| 179 | - | <dd>merge requests (when shown)</dd> | 206 | + | <dd>copy a permalink pinned to the commit</dd> |
| 180 | </div> | 207 | </div> | ||
| 181 | <div> | 208 | <div> | ||
| 182 | <dt> | 209 | <dt> | ||
| @@ -196,19 +223,21 @@ | |||||
| 196 | <dt> | 223 | <dt> | ||
| 197 | <kbd>esc</kbd> | 224 | <kbd>esc</kbd> | ||
| 198 | </dt> | 225 | </dt> | ||
| 199 | - | <dd>close panel</dd> | 226 | + | <dd>close a dialog, leave a field</dd> |
| 200 | </div> | 227 | </div> | ||
| 201 | </dl> | 228 | </dl> | ||
| 202 | </dialog> | 229 | </dialog> | ||
| 203 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 230 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 204 | <div class="panel-head"> | 231 | <div class="panel-head"> | ||
| 205 | - | <label for="refs-filter">ref></label> | 232 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 206 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 233 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 207 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 234 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 235 | + | <kbd>esc</kbd> | |||
| 236 | + | </button> | |||
| 208 | </div> | 237 | </div> | ||
| 209 | <div class="panel-cols" data-picklist> | 238 | <div class="panel-cols" data-picklist> | ||
| 210 | <section aria-label="Branches"> | 239 | <section aria-label="Branches"> | ||
| 211 | - | <h2>branches <span class="mute">2</span> | 240 | + | <h2>Branches <span class="count">2</span> |
| 212 | </h2> | 241 | </h2> | ||
| 213 | <ul> | 242 | <ul> | ||
| 214 | <li> | 243 | <li> | ||
| @@ -222,7 +251,7 @@ | |||||
| 222 | </ul> | 251 | </ul> | ||
| 223 | </section> | 252 | </section> | ||
| 224 | <section aria-label="Tags"> | 253 | <section aria-label="Tags"> | ||
| 225 | - | <h2>tags <span class="mute">1</span> | 254 | + | <h2>Tags <span class="count">1</span> |
| 226 | </h2> | 255 | </h2> | ||
| 227 | <ul> | 256 | <ul> | ||
| 228 | <li> | 257 | <li> | ||
| @@ -233,105 +262,124 @@ | |||||
| 233 | </section> | 262 | </section> | ||
| 234 | </div> | 263 | </div> | ||
| 235 | </dialog> | 264 | </dialog> | ||
| 236 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 265 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 237 | <div class="panel-head"> | 266 | <div class="panel-head"> | ||
| 238 | - | <label for="cmd-q" class="hi">:</label> | 267 | + | <label for="cmd-q" class="sr">go to</label> |
| 239 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 268 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 240 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 269 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 241 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 270 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 242 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 271 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 243 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 272 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 244 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 273 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 245 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 274 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 246 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 275 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 276 | + | <kbd>esc</kbd> | |||
| 277 | + | </button> | |||
| 247 | </div> | 278 | </div> | ||
| 248 | - | <ul id="cmd-list" class="cmd-list"> | 279 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 249 | <li data-k="go home" data-cursor> | 280 | <li data-k="go home" data-cursor> | ||
| 250 | <a href="/" tabindex="-1"> | 281 | <a href="/" tabindex="-1"> | ||
| 251 | - | <span class="k">go</span>home</a> | 282 | + | <span class="lbl">home</span> |
| 283 | + | <span class="k">go</span> | |||
| 284 | + | </a> | |||
| 252 | </li> | 285 | </li> | ||
| 253 | <li data-k="section summary"> | 286 | <li data-k="section summary"> | ||
| 254 | <a href="/files" tabindex="-1"> | 287 | <a href="/files" tabindex="-1"> | ||
| 255 | - | <span class="k">section</span>summary</a> | 288 | + | <span class="lbl">summary</span> |
| 289 | + | <span class="k">section</span> | |||
| 290 | + | </a> | |||
| 256 | </li> | 291 | </li> | ||
| 257 | - | <li data-k="section tree"> | 292 | + | <li data-k="section files"> |
| 258 | <a href="/files/tree/main" tabindex="-1"> | 293 | <a href="/files/tree/main" tabindex="-1"> | ||
| 259 | - | <span class="k">section</span>tree</a> | 294 | + | <span class="lbl">files</span> |
| 295 | + | <span class="k">section</span> | |||
| 296 | + | </a> | |||
| 260 | </li> | 297 | </li> | ||
| 261 | <li data-k="section commits"> | 298 | <li data-k="section commits"> | ||
| 262 | <a href="/files/commits/main" tabindex="-1"> | 299 | <a href="/files/commits/main" tabindex="-1"> | ||
| 263 | - | <span class="k">section</span>commits</a> | 300 | + | <span class="lbl">commits</span> |
| 301 | + | <span class="k">section</span> | |||
| 302 | + | </a> | |||
| 264 | </li> | 303 | </li> | ||
| 265 | - | <li data-k="section refs"> | 304 | + | <li data-k="section branches & tags"> |
| 266 | <a href="/files/refs" tabindex="-1"> | 305 | <a href="/files/refs" tabindex="-1"> | ||
| 267 | - | <span class="k">section</span>refs</a> | 306 | + | <span class="lbl">branches & tags</span> |
| 307 | + | <span class="k">section</span> | |||
| 308 | + | </a> | |||
| 268 | </li> | 309 | </li> | ||
| 269 | <li data-k="section merge requests"> | 310 | <li data-k="section merge requests"> | ||
| 270 | <a href="/files/merge-requests" tabindex="-1"> | 311 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 271 | - | <span class="k">section</span>merge requests</a> | 312 | + | <span class="lbl">merge requests</span> |
| 313 | + | <span class="k">section</span> | |||
| 314 | + | </a> | |||
| 272 | </li> | 315 | </li> | ||
| 273 | <li data-k="branch feature/x"> | 316 | <li data-k="branch feature/x"> | ||
| 274 | <a href="/files/tree/feature/x" tabindex="-1"> | 317 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 275 | - | <span class="k">branch</span>feature/x</a> | 318 | + | <span class="lbl">feature/x</span> |
| 319 | + | <span class="k">branch</span> | |||
| 320 | + | </a> | |||
| 276 | </li> | 321 | </li> | ||
| 277 | <li data-k="branch main"> | 322 | <li data-k="branch main"> | ||
| 278 | <a href="/files/tree/main" tabindex="-1"> | 323 | <a href="/files/tree/main" tabindex="-1"> | ||
| 279 | - | <span class="k">branch</span>main</a> | 324 | + | <span class="lbl">main</span> |
| 325 | + | <span class="k">branch</span> | |||
| 326 | + | </a> | |||
| 280 | </li> | 327 | </li> | ||
| 281 | <li data-k="tag v1.0"> | 328 | <li data-k="tag v1.0"> | ||
| 282 | <a href="/files/tree/v1.0" tabindex="-1"> | 329 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 283 | - | <span class="k">tag</span>v1.0</a> | 330 | + | <span class="lbl">v1.0</span> |
| 331 | + | <span class="k">tag</span> | |||
| 332 | + | </a> | |||
| 284 | </li> | 333 | </li> | ||
| 285 | </ul> | 334 | </ul> | ||
| 335 | + | <p class="panel-foot"> | |||
| 336 | + | <span> | |||
| 337 | + | <kbd>↑</kbd> | |||
| 338 | + | <kbd>↓</kbd> move</span> | |||
| 339 | + | <span> | |||
| 340 | + | <kbd>enter</kbd> open</span> | |||
| 341 | + | <span> | |||
| 342 | + | <kbd>esc</kbd> close</span> | |||
| 343 | + | </p> | |||
| 286 | </dialog> | 344 | </dialog> | ||
| 287 | - | <footer class="bar status" aria-label="Status"> | 345 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 288 | - | <span class="mode">blob</span> | |||
| 289 | - | <span class="st-ctx">pic.png</span> | |||
| 290 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 291 | - | <span class="hi">:</span> go to…</button> | |||
| 292 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 293 | - | </span> | |||
| 294 | - | <span class="st-hints">click a line number · y permalink · ? help</span> | |||
| 295 | - | <span class="st-src"> | |||
| 296 | - | <span class="st-ver">ermite devel · </span> | |||
| 297 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 298 | - | </span> | |||
| 299 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 300 | - | </footer> | |||
| 301 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 302 | if (q('dialog[open]').count) return; | 346 | if (q('dialog[open]').count) return; | ||
| 303 | let rows = q('[data-rows] > :not([hidden])').arr(); | 347 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 304 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 348 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 305 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 349 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 306 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 350 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 307 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 351 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 352 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 353 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 308 | })(1); | 354 | })(1); | ||
| 309 | - | k-k from:body -> ((d) => { | 355 | + | k-k from:body -> (async (d) => { |
| 310 | if (q('dialog[open]').count) return; | 356 | if (q('dialog[open]').count) return; | ||
| 311 | let rows = q('[data-rows] > :not([hidden])').arr(); | 357 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 312 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 358 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 313 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 359 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 314 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 360 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 315 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 361 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 362 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 363 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 316 | })(-1); | 364 | })(-1); | ||
| 317 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 365 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 318 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 366 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 319 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 367 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 320 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 368 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 321 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 369 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 322 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 370 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 323 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 371 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 324 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 372 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 325 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 373 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 326 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 374 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 327 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 375 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 328 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 376 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 329 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 377 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 330 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 378 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 331 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 379 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 332 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 380 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 333 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 381 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 334 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 382 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 335 | k-next from:body -> { | 383 | k-next from:body -> { | ||
| 336 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 384 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 337 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 385 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/blob-markdown.html +165 -118
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>docs/guide.md · files · example.test</title> | 7 | <title>docs/guide.md · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -41,36 +45,55 @@ | |||||
| 41 | </li> | 45 | </li> | ||
| 42 | </ol> | 46 | </ol> | ||
| 43 | </nav> | 47 | </nav> | ||
| 44 | - | <nav class="secs" aria-label="Sections"> | 48 | + | <div class="hdr-acts"> |
| 49 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 50 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="ref-name">main</span> | |||
| 53 | + | <span class="sha">SHA7</span> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 56 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 57 | + | </span> | |||
| 58 | + | <span class="search-label">Go to…</span> | |||
| 59 | + | <kbd>ctrl k</kbd> | |||
| 60 | + | </button> | |||
| 61 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 62 | + | </div> | |||
| 63 | + | </div> | |||
| 64 | + | <nav class="tabs" aria-label="Sections"> | |||
| 45 | <ul> | 65 | <ul> | ||
| 46 | <li> | 66 | <li> | ||
| 47 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 67 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 68 | + | </a> | |||
| 48 | </li> | 69 | </li> | ||
| 49 | <li> | 70 | <li> | ||
| 50 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 71 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 72 | + | </a> | |||
| 51 | </li> | 73 | </li> | ||
| 52 | <li> | 74 | <li> | ||
| 53 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 75 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 76 | + | </a> | |||
| 54 | </li> | 77 | </li> | ||
| 55 | <li> | 78 | <li> | ||
| 56 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 79 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 80 | + | </a> | |||
| 57 | </li> | 81 | </li> | ||
| 58 | <li> | 82 | <li> | ||
| 59 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 83 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 84 | + | </a> | |||
| 60 | </li> | 85 | </li> | ||
| 61 | </ul> | 86 | </ul> | ||
| 62 | </nav> | 87 | </nav> | ||
| 63 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 64 | - | </button> | |||
| 65 | </header> | 88 | </header> | ||
| 66 | - | <div class="work has-rail read"> | 89 | + | <div class="work has-rail"> |
| 67 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 90 | + | <main id="main" tabindex="-1"> |
| 68 | <h1 class="file-h"> | 91 | <h1 class="file-h"> | ||
| 69 | <span>guide.md</span> | 92 | <span>guide.md</span> | ||
| 70 | <span class="acts"> | 93 | <span class="acts"> | ||
| 71 | - | <a href="/files/raw/main/docs/guide.md">raw</a> | 94 | + | <a class="btn" href="/files/raw/main/docs/guide.md">Raw</a> |
| 72 | - | <a href="/files/commits/main/docs/guide.md">history</a> | 95 | + | <a class="btn" href="/files/commits/main/docs/guide.md">History</a> |
| 73 | - | <a href="/files/tree/main/docs">dir</a> | 96 | + | <a class="btn" href="/files/tree/main/docs">Folder</a> |
| 74 | </span> | 97 | </span> | ||
| 75 | </h1> | 98 | </h1> | ||
| 76 | <article class="prose" aria-label="guide.md"> | 99 | <article class="prose" aria-label="guide.md"> | ||
| @@ -126,10 +149,8 @@ | |||||
| 126 | </details> | 149 | </details> | ||
| 127 | </main> | 150 | </main> | ||
| 128 | <aside class="rail" aria-label="Context"> | 151 | <aside class="rail" aria-label="Context"> | ||
| 129 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 130 | - | <summary>file · last change · selection</summary> | |||
| 131 | <section aria-labelledby="r-file"> | 152 | <section aria-labelledby="r-file"> | ||
| 132 | - | <h2 id="r-file">file</h2> | 153 | + | <h2 id="r-file">File</h2> |
| 133 | <dl class="kv"> | 154 | <dl class="kv"> | ||
| 134 | <dt>size</dt> | 155 | <dt>size</dt> | ||
| 135 | <dd>47 B</dd> | 156 | <dd>47 B</dd> | ||
| @@ -140,7 +161,7 @@ | |||||
| 140 | </dl> | 161 | </dl> | ||
| 141 | </section> | 162 | </section> | ||
| 142 | <section aria-labelledby="r-last"> | 163 | <section aria-labelledby="r-last"> | ||
| 143 | - | <h2 id="r-last">last change <a href="/files/commits/main/docs/guide.md">history</a> | 164 | + | <h2 id="r-last">Last change <a href="/files/commits/main/docs/guide.md">History</a> |
| 144 | </h2> | 165 | </h2> | ||
| 145 | <p> | 166 | <p> | ||
| 146 | <a href="/files/commit/SHA40">SHA7</a> add docs/guide.md</p> | 167 | <a href="/files/commit/SHA40">SHA7</a> add docs/guide.md</p> | ||
| @@ -148,84 +169,84 @@ | |||||
| 148 | </p> | 169 | </p> | ||
| 149 | </section> | 170 | </section> | ||
| 150 | <section aria-labelledby="r-outline"> | 171 | <section aria-labelledby="r-outline"> | ||
| 151 | - | <h2 id="r-outline">outline</h2> | 172 | + | <h2 id="r-outline">On this page</h2> |
| 152 | <nav class="outline" aria-label="Outline"> | 173 | <nav class="outline" aria-label="Outline"> | ||
| 153 | <a class="l1" href="#md-guide">Guide</a> | 174 | <a class="l1" href="#md-guide">Guide</a> | ||
| 154 | <a class="l2" href="#md-steps">Steps</a> | 175 | <a class="l2" href="#md-steps">Steps</a> | ||
| 155 | </nav> | 176 | </nav> | ||
| 156 | </section> | 177 | </section> | ||
| 157 | <section aria-labelledby="r-sel"> | 178 | <section aria-labelledby="r-sel"> | ||
| 158 | - | <h2 id="r-sel">selection</h2> | 179 | + | <h2 id="r-sel">Selection</h2> |
| 159 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | 180 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | ||
| 160 | <p class="dim">y copies a permalink pinned to the commit sha</p> | 181 | <p class="dim">y copies a permalink pinned to the commit sha</p> | ||
| 161 | </section> | 182 | </section> | ||
| 162 | - | </details> | |||
| 163 | </aside> | 183 | </aside> | ||
| 164 | </div> | 184 | </div> | ||
| 165 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 185 | + | <footer class="foot"> |
| 186 | + | <span>ermite devel</span> | |||
| 187 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 188 | + | <span class="foot-keys"> | |||
| 189 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 190 | + | </footer> | |||
| 191 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 192 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 193 | + | </span> | |||
| 194 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 195 | + | </span> | |||
| 196 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 197 | + | </span> | |||
| 198 | + | </div> | |||
| 199 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 166 | <div class="panel-head"> | 200 | <div class="panel-head"> | ||
| 167 | - | <span>keys</span> | 201 | + | <h2>Keyboard shortcuts</h2> |
| 168 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 202 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 203 | + | <kbd>esc</kbd> close</button> | |||
| 169 | </div> | 204 | </div> | ||
| 170 | <dl class="keys"> | 205 | <dl class="keys"> | ||
| 171 | <div> | 206 | <div> | ||
| 172 | <dt> | 207 | <dt> | ||
| 173 | - | <kbd>j</kbd> | 208 | + | <kbd>ctrl k</kbd> |
| 174 | - | <kbd>k</kbd> | 209 | + | <kbd>:</kbd> |
| 175 | </dt> | 210 | </dt> | ||
| 176 | - | <dd>move cursor down / up</dd> | 211 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 177 | </div> | 212 | </div> | ||
| 178 | <div> | 213 | <div> | ||
| 179 | <dt> | 214 | <dt> | ||
| 180 | - | <kbd>enter</kbd> | 215 | + | <kbd>?</kbd> |
| 181 | </dt> | 216 | </dt> | ||
| 182 | - | <dd>open row under cursor</dd> | 217 | + | <dd>this list</dd> |
| 183 | </div> | 218 | </div> | ||
| 184 | <div> | 219 | <div> | ||
| 185 | <dt> | 220 | <dt> | ||
| 186 | - | <kbd>g</kbd> | 221 | + | <kbd>j</kbd> |
| 187 | - | <kbd>g</kbd> / <kbd>G</kbd> | 222 | + | <kbd>k</kbd> |
| 188 | </dt> | 223 | </dt> | ||
| 189 | - | <dd>first / last row</dd> | 224 | + | <dd>move the row cursor down / up</dd> |
| 190 | </div> | 225 | </div> | ||
| 191 | <div> | 226 | <div> | ||
| 192 | <dt> | 227 | <dt> | ||
| 193 | - | <kbd>/</kbd> | 228 | + | <kbd>enter</kbd> |
| 194 | </dt> | 229 | </dt> | ||
| 195 | - | <dd>focus the filter</dd> | 230 | + | <dd>open the row under the cursor</dd> |
| 196 | </div> | 231 | </div> | ||
| 197 | <div> | 232 | <div> | ||
| 198 | <dt> | 233 | <dt> | ||
| 199 | - | <kbd>g h</kbd> | 234 | + | <kbd>g g</kbd> |
| 200 | - | <kbd>g t</kbd> | 235 | + | <kbd>G</kbd> |
| 201 | </dt> | 236 | </dt> | ||
| 202 | - | <dd>go home / tree</dd> | 237 | + | <dd>first / last row</dd> |
| 203 | </div> | 238 | </div> | ||
| 204 | <div> | 239 | <div> | ||
| 205 | <dt> | 240 | <dt> | ||
| 206 | - | <kbd>g c</kbd> | 241 | + | <kbd>/</kbd> |
| 207 | - | <kbd>g r</kbd> | |||
| 208 | </dt> | 242 | </dt> | ||
| 209 | - | <dd>go commits / refs</dd> | 243 | + | <dd>filter the list</dd> |
| 210 | </div> | 244 | </div> | ||
| 211 | <div> | 245 | <div> | ||
| 212 | <dt> | 246 | <dt> | ||
| 213 | <kbd>b</kbd> | 247 | <kbd>b</kbd> | ||
| 214 | </dt> | 248 | </dt> | ||
| 215 | - | <dd>switch ref (branches, tags)</dd> | 249 | + | <dd>switch branch or tag</dd> |
| 216 | - | </div> | |||
| 217 | - | <div> | |||
| 218 | - | <dt> | |||
| 219 | - | <kbd>:</kbd> | |||
| 220 | - | <kbd>f</kbd> | |||
| 221 | - | </dt> | |||
| 222 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 223 | - | </div> | |||
| 224 | - | <div> | |||
| 225 | - | <dt> | |||
| 226 | - | <kbd>y</kbd> | |||
| 227 | - | </dt> | |||
| 228 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 229 | </div> | 250 | </div> | ||
| 230 | <div> | 251 | <div> | ||
| 231 | <dt> | 252 | <dt> | ||
| @@ -233,15 +254,21 @@ | |||||
| 233 | <kbd>t</kbd> | 254 | <kbd>t</kbd> | ||
| 234 | <kbd>c</kbd> | 255 | <kbd>c</kbd> | ||
| 235 | <kbd>r</kbd> | 256 | <kbd>r</kbd> | ||
| 257 | + | <kbd>m</kbd> | |||
| 236 | </dt> | 258 | </dt> | ||
| 237 | - | <dd>repo sections (when shown)</dd> | 259 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 238 | </div> | 260 | </div> | ||
| 239 | <div> | 261 | <div> | ||
| 240 | <dt> | 262 | <dt> | ||
| 241 | - | <kbd>m</kbd> | 263 | + | <kbd>g h</kbd> |
| 242 | - | <kbd>g m</kbd> | 264 | + | </dt> |
| 265 | + | <dd>home</dd> | |||
| 266 | + | </div> | |||
| 267 | + | <div> | |||
| 268 | + | <dt> | |||
| 269 | + | <kbd>y</kbd> | |||
| 243 | </dt> | 270 | </dt> | ||
| 244 | - | <dd>merge requests (when shown)</dd> | 271 | + | <dd>copy a permalink pinned to the commit</dd> |
| 245 | </div> | 272 | </div> | ||
| 246 | <div> | 273 | <div> | ||
| 247 | <dt> | 274 | <dt> | ||
| @@ -261,19 +288,21 @@ | |||||
| 261 | <dt> | 288 | <dt> | ||
| 262 | <kbd>esc</kbd> | 289 | <kbd>esc</kbd> | ||
| 263 | </dt> | 290 | </dt> | ||
| 264 | - | <dd>close panel</dd> | 291 | + | <dd>close a dialog, leave a field</dd> |
| 265 | </div> | 292 | </div> | ||
| 266 | </dl> | 293 | </dl> | ||
| 267 | </dialog> | 294 | </dialog> | ||
| 268 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 295 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 269 | <div class="panel-head"> | 296 | <div class="panel-head"> | ||
| 270 | - | <label for="refs-filter">ref></label> | 297 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 271 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 298 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 272 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 299 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 300 | + | <kbd>esc</kbd> | |||
| 301 | + | </button> | |||
| 273 | </div> | 302 | </div> | ||
| 274 | <div class="panel-cols" data-picklist> | 303 | <div class="panel-cols" data-picklist> | ||
| 275 | <section aria-label="Branches"> | 304 | <section aria-label="Branches"> | ||
| 276 | - | <h2>branches <span class="mute">2</span> | 305 | + | <h2>Branches <span class="count">2</span> |
| 277 | </h2> | 306 | </h2> | ||
| 278 | <ul> | 307 | <ul> | ||
| 279 | <li> | 308 | <li> | ||
| @@ -287,7 +316,7 @@ | |||||
| 287 | </ul> | 316 | </ul> | ||
| 288 | </section> | 317 | </section> | ||
| 289 | <section aria-label="Tags"> | 318 | <section aria-label="Tags"> | ||
| 290 | - | <h2>tags <span class="mute">1</span> | 319 | + | <h2>Tags <span class="count">1</span> |
| 291 | </h2> | 320 | </h2> | ||
| 292 | <ul> | 321 | <ul> | ||
| 293 | <li> | 322 | <li> | ||
| @@ -298,106 +327,124 @@ | |||||
| 298 | </section> | 327 | </section> | ||
| 299 | </div> | 328 | </div> | ||
| 300 | </dialog> | 329 | </dialog> | ||
| 301 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 330 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 302 | <div class="panel-head"> | 331 | <div class="panel-head"> | ||
| 303 | - | <label for="cmd-q" class="hi">:</label> | 332 | + | <label for="cmd-q" class="sr">go to</label> |
| 304 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 333 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 305 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 334 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 306 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 335 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 307 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 336 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 308 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 337 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 309 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 338 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 310 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 339 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 311 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 340 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 341 | + | <kbd>esc</kbd> | |||
| 342 | + | </button> | |||
| 312 | </div> | 343 | </div> | ||
| 313 | - | <ul id="cmd-list" class="cmd-list"> | 344 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 314 | <li data-k="go home" data-cursor> | 345 | <li data-k="go home" data-cursor> | ||
| 315 | <a href="/" tabindex="-1"> | 346 | <a href="/" tabindex="-1"> | ||
| 316 | - | <span class="k">go</span>home</a> | 347 | + | <span class="lbl">home</span> |
| 348 | + | <span class="k">go</span> | |||
| 349 | + | </a> | |||
| 317 | </li> | 350 | </li> | ||
| 318 | <li data-k="section summary"> | 351 | <li data-k="section summary"> | ||
| 319 | <a href="/files" tabindex="-1"> | 352 | <a href="/files" tabindex="-1"> | ||
| 320 | - | <span class="k">section</span>summary</a> | 353 | + | <span class="lbl">summary</span> |
| 354 | + | <span class="k">section</span> | |||
| 355 | + | </a> | |||
| 321 | </li> | 356 | </li> | ||
| 322 | - | <li data-k="section tree"> | 357 | + | <li data-k="section files"> |
| 323 | <a href="/files/tree/main" tabindex="-1"> | 358 | <a href="/files/tree/main" tabindex="-1"> | ||
| 324 | - | <span class="k">section</span>tree</a> | 359 | + | <span class="lbl">files</span> |
| 360 | + | <span class="k">section</span> | |||
| 361 | + | </a> | |||
| 325 | </li> | 362 | </li> | ||
| 326 | <li data-k="section commits"> | 363 | <li data-k="section commits"> | ||
| 327 | <a href="/files/commits/main" tabindex="-1"> | 364 | <a href="/files/commits/main" tabindex="-1"> | ||
| 328 | - | <span class="k">section</span>commits</a> | 365 | + | <span class="lbl">commits</span> |
| 366 | + | <span class="k">section</span> | |||
| 367 | + | </a> | |||
| 329 | </li> | 368 | </li> | ||
| 330 | - | <li data-k="section refs"> | 369 | + | <li data-k="section branches & tags"> |
| 331 | <a href="/files/refs" tabindex="-1"> | 370 | <a href="/files/refs" tabindex="-1"> | ||
| 332 | - | <span class="k">section</span>refs</a> | 371 | + | <span class="lbl">branches & tags</span> |
| 372 | + | <span class="k">section</span> | |||
| 373 | + | </a> | |||
| 333 | </li> | 374 | </li> | ||
| 334 | <li data-k="section merge requests"> | 375 | <li data-k="section merge requests"> | ||
| 335 | <a href="/files/merge-requests" tabindex="-1"> | 376 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 336 | - | <span class="k">section</span>merge requests</a> | 377 | + | <span class="lbl">merge requests</span> |
| 378 | + | <span class="k">section</span> | |||
| 379 | + | </a> | |||
| 337 | </li> | 380 | </li> | ||
| 338 | <li data-k="branch feature/x"> | 381 | <li data-k="branch feature/x"> | ||
| 339 | <a href="/files/tree/feature/x" tabindex="-1"> | 382 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 340 | - | <span class="k">branch</span>feature/x</a> | 383 | + | <span class="lbl">feature/x</span> |
| 384 | + | <span class="k">branch</span> | |||
| 385 | + | </a> | |||
| 341 | </li> | 386 | </li> | ||
| 342 | <li data-k="branch main"> | 387 | <li data-k="branch main"> | ||
| 343 | <a href="/files/tree/main" tabindex="-1"> | 388 | <a href="/files/tree/main" tabindex="-1"> | ||
| 344 | - | <span class="k">branch</span>main</a> | 389 | + | <span class="lbl">main</span> |
| 390 | + | <span class="k">branch</span> | |||
| 391 | + | </a> | |||
| 345 | </li> | 392 | </li> | ||
| 346 | <li data-k="tag v1.0"> | 393 | <li data-k="tag v1.0"> | ||
| 347 | <a href="/files/tree/v1.0" tabindex="-1"> | 394 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 348 | - | <span class="k">tag</span>v1.0</a> | 395 | + | <span class="lbl">v1.0</span> |
| 396 | + | <span class="k">tag</span> | |||
| 397 | + | </a> | |||
| 349 | </li> | 398 | </li> | ||
| 350 | </ul> | 399 | </ul> | ||
| 400 | + | <p class="panel-foot"> | |||
| 401 | + | <span> | |||
| 402 | + | <kbd>↑</kbd> | |||
| 403 | + | <kbd>↓</kbd> move</span> | |||
| 404 | + | <span> | |||
| 405 | + | <kbd>enter</kbd> open</span> | |||
| 406 | + | <span> | |||
| 407 | + | <kbd>esc</kbd> close</span> | |||
| 408 | + | </p> | |||
| 351 | </dialog> | 409 | </dialog> | ||
| 352 | - | <footer class="bar status" aria-label="Status"> | 410 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 353 | - | <span class="mode">blob</span> | |||
| 354 | - | <span class="st-ctx">guide.md</span> | |||
| 355 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 356 | - | <span class="hi">:</span> go to…</button> | |||
| 357 | - | <span class="st-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : 'click a line number · shift-click extends'">click a line number · shift-click extends</span> | |||
| 358 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 359 | - | </span> | |||
| 360 | - | <span class="st-hints">click a line number · y permalink · ? help</span> | |||
| 361 | - | <span class="st-src"> | |||
| 362 | - | <span class="st-ver">ermite devel · </span> | |||
| 363 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 364 | - | </span> | |||
| 365 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 366 | - | </footer> | |||
| 367 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 368 | if (q('dialog[open]').count) return; | 411 | if (q('dialog[open]').count) return; | ||
| 369 | let rows = q('[data-rows] > :not([hidden])').arr(); | 412 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 370 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 413 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 371 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 414 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 372 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 415 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 373 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 416 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 417 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 418 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 374 | })(1); | 419 | })(1); | ||
| 375 | - | k-k from:body -> ((d) => { | 420 | + | k-k from:body -> (async (d) => { |
| 376 | if (q('dialog[open]').count) return; | 421 | if (q('dialog[open]').count) return; | ||
| 377 | let rows = q('[data-rows] > :not([hidden])').arr(); | 422 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 378 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 423 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 379 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 424 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 380 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 425 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 381 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 426 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 427 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 428 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 382 | })(-1); | 429 | })(-1); | ||
| 383 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 430 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 384 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 431 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 385 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 432 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 386 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 433 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 387 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 434 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 388 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 435 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 389 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 436 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 390 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 437 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 391 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 438 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 392 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 439 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 393 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 440 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 394 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 441 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 395 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 442 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 396 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 443 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 397 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 444 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 398 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 445 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 399 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 446 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 400 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 447 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 401 | k-next from:body -> { | 448 | k-next from:body -> { | ||
| 402 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 449 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 403 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 450 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/blob-svg.html +163 -116
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>logo.svg · files · example.test</title> | 7 | <title>logo.svg · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,36 +42,55 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">main</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 68 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 72 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work has-rail"> | 86 | <div class="work has-rail"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <h1 class="file-h"> | 88 | <h1 class="file-h"> | ||
| 66 | <span>logo.svg</span> | 89 | <span>logo.svg</span> | ||
| 67 | <span class="acts"> | 90 | <span class="acts"> | ||
| 68 | - | <a href="/files/raw/main/logo.svg">raw</a> | 91 | + | <a class="btn" href="/files/raw/main/logo.svg">Raw</a> |
| 69 | - | <a href="/files/commits/main/logo.svg">history</a> | 92 | + | <a class="btn" href="/files/commits/main/logo.svg">History</a> |
| 70 | - | <a href="/files/tree/main">dir</a> | 93 | + | <a class="btn" href="/files/tree/main">Folder</a> |
| 71 | </span> | 94 | </span> | ||
| 72 | </h1> | 95 | </h1> | ||
| 73 | <pre class="chroma code" data-anchor="" hx-on:click="let a = event.target.closest('a.ln'); if (!a) return; event.preventDefault(); | 96 | <pre class="chroma code" data-anchor="" hx-on:click="let a = event.target.closest('a.ln'); if (!a) return; event.preventDefault(); | ||
| @@ -91,10 +114,8 @@ | |||||
| 91 | </pre> | 114 | </pre> | ||
| 92 | </main> | 115 | </main> | ||
| 93 | <aside class="rail" aria-label="Context"> | 116 | <aside class="rail" aria-label="Context"> | ||
| 94 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 95 | - | <summary>file · last change · selection</summary> | |||
| 96 | <section aria-labelledby="r-file"> | 117 | <section aria-labelledby="r-file"> | ||
| 97 | - | <h2 id="r-file">file</h2> | 118 | + | <h2 id="r-file">File</h2> |
| 98 | <dl class="kv"> | 119 | <dl class="kv"> | ||
| 99 | <dt>size</dt> | 120 | <dt>size</dt> | ||
| 100 | <dd>89 B</dd> | 121 | <dd>89 B</dd> | ||
| @@ -105,7 +126,7 @@ | |||||
| 105 | </dl> | 126 | </dl> | ||
| 106 | </section> | 127 | </section> | ||
| 107 | <section aria-labelledby="r-last"> | 128 | <section aria-labelledby="r-last"> | ||
| 108 | - | <h2 id="r-last">last change <a href="/files/commits/main/logo.svg">history</a> | 129 | + | <h2 id="r-last">Last change <a href="/files/commits/main/logo.svg">History</a> |
| 109 | </h2> | 130 | </h2> | ||
| 110 | <p> | 131 | <p> | ||
| 111 | <a href="/files/commit/SHA40">SHA7</a> add logo.svg</p> | 132 | <a href="/files/commit/SHA40">SHA7</a> add logo.svg</p> | ||
| @@ -113,77 +134,77 @@ | |||||
| 113 | </p> | 134 | </p> | ||
| 114 | </section> | 135 | </section> | ||
| 115 | <section aria-labelledby="r-sel"> | 136 | <section aria-labelledby="r-sel"> | ||
| 116 | - | <h2 id="r-sel">selection</h2> | 137 | + | <h2 id="r-sel">Selection</h2> |
| 117 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | 138 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | ||
| 118 | <p class="dim">y copies a permalink pinned to the commit sha</p> | 139 | <p class="dim">y copies a permalink pinned to the commit sha</p> | ||
| 119 | </section> | 140 | </section> | ||
| 120 | - | </details> | |||
| 121 | </aside> | 141 | </aside> | ||
| 122 | </div> | 142 | </div> | ||
| 123 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 143 | + | <footer class="foot"> |
| 144 | + | <span>ermite devel</span> | |||
| 145 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 146 | + | <span class="foot-keys"> | |||
| 147 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 148 | + | </footer> | |||
| 149 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 150 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 151 | + | </span> | |||
| 152 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 153 | + | </span> | |||
| 154 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 155 | + | </span> | |||
| 156 | + | </div> | |||
| 157 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 124 | <div class="panel-head"> | 158 | <div class="panel-head"> | ||
| 125 | - | <span>keys</span> | 159 | + | <h2>Keyboard shortcuts</h2> |
| 126 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 160 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 161 | + | <kbd>esc</kbd> close</button> | |||
| 127 | </div> | 162 | </div> | ||
| 128 | <dl class="keys"> | 163 | <dl class="keys"> | ||
| 129 | <div> | 164 | <div> | ||
| 130 | <dt> | 165 | <dt> | ||
| 131 | - | <kbd>j</kbd> | 166 | + | <kbd>ctrl k</kbd> |
| 132 | - | <kbd>k</kbd> | 167 | + | <kbd>:</kbd> |
| 133 | </dt> | 168 | </dt> | ||
| 134 | - | <dd>move cursor down / up</dd> | 169 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 135 | </div> | 170 | </div> | ||
| 136 | <div> | 171 | <div> | ||
| 137 | <dt> | 172 | <dt> | ||
| 138 | - | <kbd>enter</kbd> | 173 | + | <kbd>?</kbd> |
| 139 | </dt> | 174 | </dt> | ||
| 140 | - | <dd>open row under cursor</dd> | 175 | + | <dd>this list</dd> |
| 141 | </div> | 176 | </div> | ||
| 142 | <div> | 177 | <div> | ||
| 143 | <dt> | 178 | <dt> | ||
| 144 | - | <kbd>g</kbd> | 179 | + | <kbd>j</kbd> |
| 145 | - | <kbd>g</kbd> / <kbd>G</kbd> | 180 | + | <kbd>k</kbd> |
| 146 | </dt> | 181 | </dt> | ||
| 147 | - | <dd>first / last row</dd> | 182 | + | <dd>move the row cursor down / up</dd> |
| 148 | </div> | 183 | </div> | ||
| 149 | <div> | 184 | <div> | ||
| 150 | <dt> | 185 | <dt> | ||
| 151 | - | <kbd>/</kbd> | 186 | + | <kbd>enter</kbd> |
| 152 | </dt> | 187 | </dt> | ||
| 153 | - | <dd>focus the filter</dd> | 188 | + | <dd>open the row under the cursor</dd> |
| 154 | </div> | 189 | </div> | ||
| 155 | <div> | 190 | <div> | ||
| 156 | <dt> | 191 | <dt> | ||
| 157 | - | <kbd>g h</kbd> | 192 | + | <kbd>g g</kbd> |
| 158 | - | <kbd>g t</kbd> | 193 | + | <kbd>G</kbd> |
| 159 | </dt> | 194 | </dt> | ||
| 160 | - | <dd>go home / tree</dd> | 195 | + | <dd>first / last row</dd> |
| 161 | </div> | 196 | </div> | ||
| 162 | <div> | 197 | <div> | ||
| 163 | <dt> | 198 | <dt> | ||
| 164 | - | <kbd>g c</kbd> | 199 | + | <kbd>/</kbd> |
| 165 | - | <kbd>g r</kbd> | |||
| 166 | </dt> | 200 | </dt> | ||
| 167 | - | <dd>go commits / refs</dd> | 201 | + | <dd>filter the list</dd> |
| 168 | </div> | 202 | </div> | ||
| 169 | <div> | 203 | <div> | ||
| 170 | <dt> | 204 | <dt> | ||
| 171 | <kbd>b</kbd> | 205 | <kbd>b</kbd> | ||
| 172 | </dt> | 206 | </dt> | ||
| 173 | - | <dd>switch ref (branches, tags)</dd> | 207 | + | <dd>switch branch or tag</dd> |
| 174 | - | </div> | |||
| 175 | - | <div> | |||
| 176 | - | <dt> | |||
| 177 | - | <kbd>:</kbd> | |||
| 178 | - | <kbd>f</kbd> | |||
| 179 | - | </dt> | |||
| 180 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 181 | - | </div> | |||
| 182 | - | <div> | |||
| 183 | - | <dt> | |||
| 184 | - | <kbd>y</kbd> | |||
| 185 | - | </dt> | |||
| 186 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 187 | </div> | 208 | </div> | ||
| 188 | <div> | 209 | <div> | ||
| 189 | <dt> | 210 | <dt> | ||
| @@ -191,15 +212,21 @@ | |||||
| 191 | <kbd>t</kbd> | 212 | <kbd>t</kbd> | ||
| 192 | <kbd>c</kbd> | 213 | <kbd>c</kbd> | ||
| 193 | <kbd>r</kbd> | 214 | <kbd>r</kbd> | ||
| 215 | + | <kbd>m</kbd> | |||
| 194 | </dt> | 216 | </dt> | ||
| 195 | - | <dd>repo sections (when shown)</dd> | 217 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 196 | </div> | 218 | </div> | ||
| 197 | <div> | 219 | <div> | ||
| 198 | <dt> | 220 | <dt> | ||
| 199 | - | <kbd>m</kbd> | 221 | + | <kbd>g h</kbd> |
| 200 | - | <kbd>g m</kbd> | 222 | + | </dt> |
| 223 | + | <dd>home</dd> | |||
| 224 | + | </div> | |||
| 225 | + | <div> | |||
| 226 | + | <dt> | |||
| 227 | + | <kbd>y</kbd> | |||
| 201 | </dt> | 228 | </dt> | ||
| 202 | - | <dd>merge requests (when shown)</dd> | 229 | + | <dd>copy a permalink pinned to the commit</dd> |
| 203 | </div> | 230 | </div> | ||
| 204 | <div> | 231 | <div> | ||
| 205 | <dt> | 232 | <dt> | ||
| @@ -219,19 +246,21 @@ | |||||
| 219 | <dt> | 246 | <dt> | ||
| 220 | <kbd>esc</kbd> | 247 | <kbd>esc</kbd> | ||
| 221 | </dt> | 248 | </dt> | ||
| 222 | - | <dd>close panel</dd> | 249 | + | <dd>close a dialog, leave a field</dd> |
| 223 | </div> | 250 | </div> | ||
| 224 | </dl> | 251 | </dl> | ||
| 225 | </dialog> | 252 | </dialog> | ||
| 226 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 253 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 227 | <div class="panel-head"> | 254 | <div class="panel-head"> | ||
| 228 | - | <label for="refs-filter">ref></label> | 255 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 229 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 256 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 230 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 257 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 258 | + | <kbd>esc</kbd> | |||
| 259 | + | </button> | |||
| 231 | </div> | 260 | </div> | ||
| 232 | <div class="panel-cols" data-picklist> | 261 | <div class="panel-cols" data-picklist> | ||
| 233 | <section aria-label="Branches"> | 262 | <section aria-label="Branches"> | ||
| 234 | - | <h2>branches <span class="mute">2</span> | 263 | + | <h2>Branches <span class="count">2</span> |
| 235 | </h2> | 264 | </h2> | ||
| 236 | <ul> | 265 | <ul> | ||
| 237 | <li> | 266 | <li> | ||
| @@ -245,7 +274,7 @@ | |||||
| 245 | </ul> | 274 | </ul> | ||
| 246 | </section> | 275 | </section> | ||
| 247 | <section aria-label="Tags"> | 276 | <section aria-label="Tags"> | ||
| 248 | - | <h2>tags <span class="mute">1</span> | 277 | + | <h2>Tags <span class="count">1</span> |
| 249 | </h2> | 278 | </h2> | ||
| 250 | <ul> | 279 | <ul> | ||
| 251 | <li> | 280 | <li> | ||
| @@ -256,106 +285,124 @@ | |||||
| 256 | </section> | 285 | </section> | ||
| 257 | </div> | 286 | </div> | ||
| 258 | </dialog> | 287 | </dialog> | ||
| 259 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 288 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 260 | <div class="panel-head"> | 289 | <div class="panel-head"> | ||
| 261 | - | <label for="cmd-q" class="hi">:</label> | 290 | + | <label for="cmd-q" class="sr">go to</label> |
| 262 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 291 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 263 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 292 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 264 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 293 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 265 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 294 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 266 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 295 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 267 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 296 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 268 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 297 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 269 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 298 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 299 | + | <kbd>esc</kbd> | |||
| 300 | + | </button> | |||
| 270 | </div> | 301 | </div> | ||
| 271 | - | <ul id="cmd-list" class="cmd-list"> | 302 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 272 | <li data-k="go home" data-cursor> | 303 | <li data-k="go home" data-cursor> | ||
| 273 | <a href="/" tabindex="-1"> | 304 | <a href="/" tabindex="-1"> | ||
| 274 | - | <span class="k">go</span>home</a> | 305 | + | <span class="lbl">home</span> |
| 306 | + | <span class="k">go</span> | |||
| 307 | + | </a> | |||
| 275 | </li> | 308 | </li> | ||
| 276 | <li data-k="section summary"> | 309 | <li data-k="section summary"> | ||
| 277 | <a href="/files" tabindex="-1"> | 310 | <a href="/files" tabindex="-1"> | ||
| 278 | - | <span class="k">section</span>summary</a> | 311 | + | <span class="lbl">summary</span> |
| 312 | + | <span class="k">section</span> | |||
| 313 | + | </a> | |||
| 279 | </li> | 314 | </li> | ||
| 280 | - | <li data-k="section tree"> | 315 | + | <li data-k="section files"> |
| 281 | <a href="/files/tree/main" tabindex="-1"> | 316 | <a href="/files/tree/main" tabindex="-1"> | ||
| 282 | - | <span class="k">section</span>tree</a> | 317 | + | <span class="lbl">files</span> |
| 318 | + | <span class="k">section</span> | |||
| 319 | + | </a> | |||
| 283 | </li> | 320 | </li> | ||
| 284 | <li data-k="section commits"> | 321 | <li data-k="section commits"> | ||
| 285 | <a href="/files/commits/main" tabindex="-1"> | 322 | <a href="/files/commits/main" tabindex="-1"> | ||
| 286 | - | <span class="k">section</span>commits</a> | 323 | + | <span class="lbl">commits</span> |
| 324 | + | <span class="k">section</span> | |||
| 325 | + | </a> | |||
| 287 | </li> | 326 | </li> | ||
| 288 | - | <li data-k="section refs"> | 327 | + | <li data-k="section branches & tags"> |
| 289 | <a href="/files/refs" tabindex="-1"> | 328 | <a href="/files/refs" tabindex="-1"> | ||
| 290 | - | <span class="k">section</span>refs</a> | 329 | + | <span class="lbl">branches & tags</span> |
| 330 | + | <span class="k">section</span> | |||
| 331 | + | </a> | |||
| 291 | </li> | 332 | </li> | ||
| 292 | <li data-k="section merge requests"> | 333 | <li data-k="section merge requests"> | ||
| 293 | <a href="/files/merge-requests" tabindex="-1"> | 334 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 294 | - | <span class="k">section</span>merge requests</a> | 335 | + | <span class="lbl">merge requests</span> |
| 336 | + | <span class="k">section</span> | |||
| 337 | + | </a> | |||
| 295 | </li> | 338 | </li> | ||
| 296 | <li data-k="branch feature/x"> | 339 | <li data-k="branch feature/x"> | ||
| 297 | <a href="/files/tree/feature/x" tabindex="-1"> | 340 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 298 | - | <span class="k">branch</span>feature/x</a> | 341 | + | <span class="lbl">feature/x</span> |
| 342 | + | <span class="k">branch</span> | |||
| 343 | + | </a> | |||
| 299 | </li> | 344 | </li> | ||
| 300 | <li data-k="branch main"> | 345 | <li data-k="branch main"> | ||
| 301 | <a href="/files/tree/main" tabindex="-1"> | 346 | <a href="/files/tree/main" tabindex="-1"> | ||
| 302 | - | <span class="k">branch</span>main</a> | 347 | + | <span class="lbl">main</span> |
| 348 | + | <span class="k">branch</span> | |||
| 349 | + | </a> | |||
| 303 | </li> | 350 | </li> | ||
| 304 | <li data-k="tag v1.0"> | 351 | <li data-k="tag v1.0"> | ||
| 305 | <a href="/files/tree/v1.0" tabindex="-1"> | 352 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 306 | - | <span class="k">tag</span>v1.0</a> | 353 | + | <span class="lbl">v1.0</span> |
| 354 | + | <span class="k">tag</span> | |||
| 355 | + | </a> | |||
| 307 | </li> | 356 | </li> | ||
| 308 | </ul> | 357 | </ul> | ||
| 358 | + | <p class="panel-foot"> | |||
| 359 | + | <span> | |||
| 360 | + | <kbd>↑</kbd> | |||
| 361 | + | <kbd>↓</kbd> move</span> | |||
| 362 | + | <span> | |||
| 363 | + | <kbd>enter</kbd> open</span> | |||
| 364 | + | <span> | |||
| 365 | + | <kbd>esc</kbd> close</span> | |||
| 366 | + | </p> | |||
| 309 | </dialog> | 367 | </dialog> | ||
| 310 | - | <footer class="bar status" aria-label="Status"> | 368 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 311 | - | <span class="mode">blob</span> | |||
| 312 | - | <span class="st-ctx">logo.svg</span> | |||
| 313 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 314 | - | <span class="hi">:</span> go to…</button> | |||
| 315 | - | <span class="st-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : 'click a line number · shift-click extends'">click a line number · shift-click extends</span> | |||
| 316 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 317 | - | </span> | |||
| 318 | - | <span class="st-hints">click a line number · y permalink · ? help</span> | |||
| 319 | - | <span class="st-src"> | |||
| 320 | - | <span class="st-ver">ermite devel · </span> | |||
| 321 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 322 | - | </span> | |||
| 323 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 324 | - | </footer> | |||
| 325 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 326 | if (q('dialog[open]').count) return; | 369 | if (q('dialog[open]').count) return; | ||
| 327 | let rows = q('[data-rows] > :not([hidden])').arr(); | 370 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 328 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 371 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 329 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 372 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 330 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 373 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 331 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 374 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 375 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 376 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 332 | })(1); | 377 | })(1); | ||
| 333 | - | k-k from:body -> ((d) => { | 378 | + | k-k from:body -> (async (d) => { |
| 334 | if (q('dialog[open]').count) return; | 379 | if (q('dialog[open]').count) return; | ||
| 335 | let rows = q('[data-rows] > :not([hidden])').arr(); | 380 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 336 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 381 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 337 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 382 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 338 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 383 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 339 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 384 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 385 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 386 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 340 | })(-1); | 387 | })(-1); | ||
| 341 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 388 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 342 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 389 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 343 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 390 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 344 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 391 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 345 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 392 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 346 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 393 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 347 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 394 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 348 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 395 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 349 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 396 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 350 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 397 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 351 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 398 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 352 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 399 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 353 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 400 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 354 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 401 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 355 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 402 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 356 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 403 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 357 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 404 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 358 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 405 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 359 | k-next from:body -> { | 406 | k-next from:body -> { | ||
| 360 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 407 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 361 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 408 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/blob-text.html +163 -116
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>notes.txt · files · example.test</title> | 7 | <title>notes.txt · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,36 +42,55 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">main</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 68 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 72 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work has-rail"> | 86 | <div class="work has-rail"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <h1 class="file-h"> | 88 | <h1 class="file-h"> | ||
| 66 | <span>notes.txt</span> | 89 | <span>notes.txt</span> | ||
| 67 | <span class="acts"> | 90 | <span class="acts"> | ||
| 68 | - | <a href="/files/raw/main/notes.txt">raw</a> | 91 | + | <a class="btn" href="/files/raw/main/notes.txt">Raw</a> |
| 69 | - | <a href="/files/commits/main/notes.txt">history</a> | 92 | + | <a class="btn" href="/files/commits/main/notes.txt">History</a> |
| 70 | - | <a href="/files/tree/main">dir</a> | 93 | + | <a class="btn" href="/files/tree/main">Folder</a> |
| 71 | </span> | 94 | </span> | ||
| 72 | </h1> | 95 | </h1> | ||
| 73 | <pre class="chroma code" data-anchor="" hx-on:click="let a = event.target.closest('a.ln'); if (!a) return; event.preventDefault(); | 96 | <pre class="chroma code" data-anchor="" hx-on:click="let a = event.target.closest('a.ln'); if (!a) return; event.preventDefault(); | ||
| @@ -86,10 +109,8 @@ | |||||
| 86 | </pre> | 109 | </pre> | ||
| 87 | </main> | 110 | </main> | ||
| 88 | <aside class="rail" aria-label="Context"> | 111 | <aside class="rail" aria-label="Context"> | ||
| 89 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 90 | - | <summary>file · last change · selection</summary> | |||
| 91 | <section aria-labelledby="r-file"> | 112 | <section aria-labelledby="r-file"> | ||
| 92 | - | <h2 id="r-file">file</h2> | 113 | + | <h2 id="r-file">File</h2> |
| 93 | <dl class="kv"> | 114 | <dl class="kv"> | ||
| 94 | <dt>size</dt> | 115 | <dt>size</dt> | ||
| 95 | <dd>24 B</dd> | 116 | <dd>24 B</dd> | ||
| @@ -100,7 +121,7 @@ | |||||
| 100 | </dl> | 121 | </dl> | ||
| 101 | </section> | 122 | </section> | ||
| 102 | <section aria-labelledby="r-last"> | 123 | <section aria-labelledby="r-last"> | ||
| 103 | - | <h2 id="r-last">last change <a href="/files/commits/main/notes.txt">history</a> | 124 | + | <h2 id="r-last">Last change <a href="/files/commits/main/notes.txt">History</a> |
| 104 | </h2> | 125 | </h2> | ||
| 105 | <p> | 126 | <p> | ||
| 106 | <a href="/files/commit/SHA40">SHA7</a> add notes.txt</p> | 127 | <a href="/files/commit/SHA40">SHA7</a> add notes.txt</p> | ||
| @@ -108,77 +129,77 @@ | |||||
| 108 | </p> | 129 | </p> | ||
| 109 | </section> | 130 | </section> | ||
| 110 | <section aria-labelledby="r-sel"> | 131 | <section aria-labelledby="r-sel"> | ||
| 111 | - | <h2 id="r-sel">selection</h2> | 132 | + | <h2 id="r-sel">Selection</h2> |
| 112 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | 133 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | ||
| 113 | <p class="dim">y copies a permalink pinned to the commit sha</p> | 134 | <p class="dim">y copies a permalink pinned to the commit sha</p> | ||
| 114 | </section> | 135 | </section> | ||
| 115 | - | </details> | |||
| 116 | </aside> | 136 | </aside> | ||
| 117 | </div> | 137 | </div> | ||
| 118 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 138 | + | <footer class="foot"> |
| 139 | + | <span>ermite devel</span> | |||
| 140 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 141 | + | <span class="foot-keys"> | |||
| 142 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 143 | + | </footer> | |||
| 144 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 145 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 146 | + | </span> | |||
| 147 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 148 | + | </span> | |||
| 149 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 150 | + | </span> | |||
| 151 | + | </div> | |||
| 152 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 119 | <div class="panel-head"> | 153 | <div class="panel-head"> | ||
| 120 | - | <span>keys</span> | 154 | + | <h2>Keyboard shortcuts</h2> |
| 121 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 155 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 156 | + | <kbd>esc</kbd> close</button> | |||
| 122 | </div> | 157 | </div> | ||
| 123 | <dl class="keys"> | 158 | <dl class="keys"> | ||
| 124 | <div> | 159 | <div> | ||
| 125 | <dt> | 160 | <dt> | ||
| 126 | - | <kbd>j</kbd> | 161 | + | <kbd>ctrl k</kbd> |
| 127 | - | <kbd>k</kbd> | 162 | + | <kbd>:</kbd> |
| 128 | </dt> | 163 | </dt> | ||
| 129 | - | <dd>move cursor down / up</dd> | 164 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 130 | </div> | 165 | </div> | ||
| 131 | <div> | 166 | <div> | ||
| 132 | <dt> | 167 | <dt> | ||
| 133 | - | <kbd>enter</kbd> | 168 | + | <kbd>?</kbd> |
| 134 | </dt> | 169 | </dt> | ||
| 135 | - | <dd>open row under cursor</dd> | 170 | + | <dd>this list</dd> |
| 136 | </div> | 171 | </div> | ||
| 137 | <div> | 172 | <div> | ||
| 138 | <dt> | 173 | <dt> | ||
| 139 | - | <kbd>g</kbd> | 174 | + | <kbd>j</kbd> |
| 140 | - | <kbd>g</kbd> / <kbd>G</kbd> | 175 | + | <kbd>k</kbd> |
| 141 | </dt> | 176 | </dt> | ||
| 142 | - | <dd>first / last row</dd> | 177 | + | <dd>move the row cursor down / up</dd> |
| 143 | </div> | 178 | </div> | ||
| 144 | <div> | 179 | <div> | ||
| 145 | <dt> | 180 | <dt> | ||
| 146 | - | <kbd>/</kbd> | 181 | + | <kbd>enter</kbd> |
| 147 | </dt> | 182 | </dt> | ||
| 148 | - | <dd>focus the filter</dd> | 183 | + | <dd>open the row under the cursor</dd> |
| 149 | </div> | 184 | </div> | ||
| 150 | <div> | 185 | <div> | ||
| 151 | <dt> | 186 | <dt> | ||
| 152 | - | <kbd>g h</kbd> | 187 | + | <kbd>g g</kbd> |
| 153 | - | <kbd>g t</kbd> | 188 | + | <kbd>G</kbd> |
| 154 | </dt> | 189 | </dt> | ||
| 155 | - | <dd>go home / tree</dd> | 190 | + | <dd>first / last row</dd> |
| 156 | </div> | 191 | </div> | ||
| 157 | <div> | 192 | <div> | ||
| 158 | <dt> | 193 | <dt> | ||
| 159 | - | <kbd>g c</kbd> | 194 | + | <kbd>/</kbd> |
| 160 | - | <kbd>g r</kbd> | |||
| 161 | </dt> | 195 | </dt> | ||
| 162 | - | <dd>go commits / refs</dd> | 196 | + | <dd>filter the list</dd> |
| 163 | </div> | 197 | </div> | ||
| 164 | <div> | 198 | <div> | ||
| 165 | <dt> | 199 | <dt> | ||
| 166 | <kbd>b</kbd> | 200 | <kbd>b</kbd> | ||
| 167 | </dt> | 201 | </dt> | ||
| 168 | - | <dd>switch ref (branches, tags)</dd> | 202 | + | <dd>switch branch or tag</dd> |
| 169 | - | </div> | |||
| 170 | - | <div> | |||
| 171 | - | <dt> | |||
| 172 | - | <kbd>:</kbd> | |||
| 173 | - | <kbd>f</kbd> | |||
| 174 | - | </dt> | |||
| 175 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 176 | - | </div> | |||
| 177 | - | <div> | |||
| 178 | - | <dt> | |||
| 179 | - | <kbd>y</kbd> | |||
| 180 | - | </dt> | |||
| 181 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 182 | </div> | 203 | </div> | ||
| 183 | <div> | 204 | <div> | ||
| 184 | <dt> | 205 | <dt> | ||
| @@ -186,15 +207,21 @@ | |||||
| 186 | <kbd>t</kbd> | 207 | <kbd>t</kbd> | ||
| 187 | <kbd>c</kbd> | 208 | <kbd>c</kbd> | ||
| 188 | <kbd>r</kbd> | 209 | <kbd>r</kbd> | ||
| 210 | + | <kbd>m</kbd> | |||
| 189 | </dt> | 211 | </dt> | ||
| 190 | - | <dd>repo sections (when shown)</dd> | 212 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 191 | </div> | 213 | </div> | ||
| 192 | <div> | 214 | <div> | ||
| 193 | <dt> | 215 | <dt> | ||
| 194 | - | <kbd>m</kbd> | 216 | + | <kbd>g h</kbd> |
| 195 | - | <kbd>g m</kbd> | 217 | + | </dt> |
| 218 | + | <dd>home</dd> | |||
| 219 | + | </div> | |||
| 220 | + | <div> | |||
| 221 | + | <dt> | |||
| 222 | + | <kbd>y</kbd> | |||
| 196 | </dt> | 223 | </dt> | ||
| 197 | - | <dd>merge requests (when shown)</dd> | 224 | + | <dd>copy a permalink pinned to the commit</dd> |
| 198 | </div> | 225 | </div> | ||
| 199 | <div> | 226 | <div> | ||
| 200 | <dt> | 227 | <dt> | ||
| @@ -214,19 +241,21 @@ | |||||
| 214 | <dt> | 241 | <dt> | ||
| 215 | <kbd>esc</kbd> | 242 | <kbd>esc</kbd> | ||
| 216 | </dt> | 243 | </dt> | ||
| 217 | - | <dd>close panel</dd> | 244 | + | <dd>close a dialog, leave a field</dd> |
| 218 | </div> | 245 | </div> | ||
| 219 | </dl> | 246 | </dl> | ||
| 220 | </dialog> | 247 | </dialog> | ||
| 221 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 248 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 222 | <div class="panel-head"> | 249 | <div class="panel-head"> | ||
| 223 | - | <label for="refs-filter">ref></label> | 250 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 224 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 251 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 225 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 252 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 253 | + | <kbd>esc</kbd> | |||
| 254 | + | </button> | |||
| 226 | </div> | 255 | </div> | ||
| 227 | <div class="panel-cols" data-picklist> | 256 | <div class="panel-cols" data-picklist> | ||
| 228 | <section aria-label="Branches"> | 257 | <section aria-label="Branches"> | ||
| 229 | - | <h2>branches <span class="mute">2</span> | 258 | + | <h2>Branches <span class="count">2</span> |
| 230 | </h2> | 259 | </h2> | ||
| 231 | <ul> | 260 | <ul> | ||
| 232 | <li> | 261 | <li> | ||
| @@ -240,7 +269,7 @@ | |||||
| 240 | </ul> | 269 | </ul> | ||
| 241 | </section> | 270 | </section> | ||
| 242 | <section aria-label="Tags"> | 271 | <section aria-label="Tags"> | ||
| 243 | - | <h2>tags <span class="mute">1</span> | 272 | + | <h2>Tags <span class="count">1</span> |
| 244 | </h2> | 273 | </h2> | ||
| 245 | <ul> | 274 | <ul> | ||
| 246 | <li> | 275 | <li> | ||
| @@ -251,106 +280,124 @@ | |||||
| 251 | </section> | 280 | </section> | ||
| 252 | </div> | 281 | </div> | ||
| 253 | </dialog> | 282 | </dialog> | ||
| 254 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 283 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 255 | <div class="panel-head"> | 284 | <div class="panel-head"> | ||
| 256 | - | <label for="cmd-q" class="hi">:</label> | 285 | + | <label for="cmd-q" class="sr">go to</label> |
| 257 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 286 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 258 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 287 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 259 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 288 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 260 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 289 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 261 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 290 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 262 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 291 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 263 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 292 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 264 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 293 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 294 | + | <kbd>esc</kbd> | |||
| 295 | + | </button> | |||
| 265 | </div> | 296 | </div> | ||
| 266 | - | <ul id="cmd-list" class="cmd-list"> | 297 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 267 | <li data-k="go home" data-cursor> | 298 | <li data-k="go home" data-cursor> | ||
| 268 | <a href="/" tabindex="-1"> | 299 | <a href="/" tabindex="-1"> | ||
| 269 | - | <span class="k">go</span>home</a> | 300 | + | <span class="lbl">home</span> |
| 301 | + | <span class="k">go</span> | |||
| 302 | + | </a> | |||
| 270 | </li> | 303 | </li> | ||
| 271 | <li data-k="section summary"> | 304 | <li data-k="section summary"> | ||
| 272 | <a href="/files" tabindex="-1"> | 305 | <a href="/files" tabindex="-1"> | ||
| 273 | - | <span class="k">section</span>summary</a> | 306 | + | <span class="lbl">summary</span> |
| 307 | + | <span class="k">section</span> | |||
| 308 | + | </a> | |||
| 274 | </li> | 309 | </li> | ||
| 275 | - | <li data-k="section tree"> | 310 | + | <li data-k="section files"> |
| 276 | <a href="/files/tree/main" tabindex="-1"> | 311 | <a href="/files/tree/main" tabindex="-1"> | ||
| 277 | - | <span class="k">section</span>tree</a> | 312 | + | <span class="lbl">files</span> |
| 313 | + | <span class="k">section</span> | |||
| 314 | + | </a> | |||
| 278 | </li> | 315 | </li> | ||
| 279 | <li data-k="section commits"> | 316 | <li data-k="section commits"> | ||
| 280 | <a href="/files/commits/main" tabindex="-1"> | 317 | <a href="/files/commits/main" tabindex="-1"> | ||
| 281 | - | <span class="k">section</span>commits</a> | 318 | + | <span class="lbl">commits</span> |
| 319 | + | <span class="k">section</span> | |||
| 320 | + | </a> | |||
| 282 | </li> | 321 | </li> | ||
| 283 | - | <li data-k="section refs"> | 322 | + | <li data-k="section branches & tags"> |
| 284 | <a href="/files/refs" tabindex="-1"> | 323 | <a href="/files/refs" tabindex="-1"> | ||
| 285 | - | <span class="k">section</span>refs</a> | 324 | + | <span class="lbl">branches & tags</span> |
| 325 | + | <span class="k">section</span> | |||
| 326 | + | </a> | |||
| 286 | </li> | 327 | </li> | ||
| 287 | <li data-k="section merge requests"> | 328 | <li data-k="section merge requests"> | ||
| 288 | <a href="/files/merge-requests" tabindex="-1"> | 329 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 289 | - | <span class="k">section</span>merge requests</a> | 330 | + | <span class="lbl">merge requests</span> |
| 331 | + | <span class="k">section</span> | |||
| 332 | + | </a> | |||
| 290 | </li> | 333 | </li> | ||
| 291 | <li data-k="branch feature/x"> | 334 | <li data-k="branch feature/x"> | ||
| 292 | <a href="/files/tree/feature/x" tabindex="-1"> | 335 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 293 | - | <span class="k">branch</span>feature/x</a> | 336 | + | <span class="lbl">feature/x</span> |
| 337 | + | <span class="k">branch</span> | |||
| 338 | + | </a> | |||
| 294 | </li> | 339 | </li> | ||
| 295 | <li data-k="branch main"> | 340 | <li data-k="branch main"> | ||
| 296 | <a href="/files/tree/main" tabindex="-1"> | 341 | <a href="/files/tree/main" tabindex="-1"> | ||
| 297 | - | <span class="k">branch</span>main</a> | 342 | + | <span class="lbl">main</span> |
| 343 | + | <span class="k">branch</span> | |||
| 344 | + | </a> | |||
| 298 | </li> | 345 | </li> | ||
| 299 | <li data-k="tag v1.0"> | 346 | <li data-k="tag v1.0"> | ||
| 300 | <a href="/files/tree/v1.0" tabindex="-1"> | 347 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 301 | - | <span class="k">tag</span>v1.0</a> | 348 | + | <span class="lbl">v1.0</span> |
| 349 | + | <span class="k">tag</span> | |||
| 350 | + | </a> | |||
| 302 | </li> | 351 | </li> | ||
| 303 | </ul> | 352 | </ul> | ||
| 353 | + | <p class="panel-foot"> | |||
| 354 | + | <span> | |||
| 355 | + | <kbd>↑</kbd> | |||
| 356 | + | <kbd>↓</kbd> move</span> | |||
| 357 | + | <span> | |||
| 358 | + | <kbd>enter</kbd> open</span> | |||
| 359 | + | <span> | |||
| 360 | + | <kbd>esc</kbd> close</span> | |||
| 361 | + | </p> | |||
| 304 | </dialog> | 362 | </dialog> | ||
| 305 | - | <footer class="bar status" aria-label="Status"> | 363 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 306 | - | <span class="mode">blob</span> | |||
| 307 | - | <span class="st-ctx">notes.txt</span> | |||
| 308 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 309 | - | <span class="hi">:</span> go to…</button> | |||
| 310 | - | <span class="st-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : 'click a line number · shift-click extends'">click a line number · shift-click extends</span> | |||
| 311 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 312 | - | </span> | |||
| 313 | - | <span class="st-hints">click a line number · y permalink · ? help</span> | |||
| 314 | - | <span class="st-src"> | |||
| 315 | - | <span class="st-ver">ermite devel · </span> | |||
| 316 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 317 | - | </span> | |||
| 318 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 319 | - | </footer> | |||
| 320 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 321 | if (q('dialog[open]').count) return; | 364 | if (q('dialog[open]').count) return; | ||
| 322 | let rows = q('[data-rows] > :not([hidden])').arr(); | 365 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 323 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 366 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 324 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 367 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 325 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 368 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 326 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 369 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 370 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 371 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 327 | })(1); | 372 | })(1); | ||
| 328 | - | k-k from:body -> ((d) => { | 373 | + | k-k from:body -> (async (d) => { |
| 329 | if (q('dialog[open]').count) return; | 374 | if (q('dialog[open]').count) return; | ||
| 330 | let rows = q('[data-rows] > :not([hidden])').arr(); | 375 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 331 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 376 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 332 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 377 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 333 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 378 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 334 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 379 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 380 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 381 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 335 | })(-1); | 382 | })(-1); | ||
| 336 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 383 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 337 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 384 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 338 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 385 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 339 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 386 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 340 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 387 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 341 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 388 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 342 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 389 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 343 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 390 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 344 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 391 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 345 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 392 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 346 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 393 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 347 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 394 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 348 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 395 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 349 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 396 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 350 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 397 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 351 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 398 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 352 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 399 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 353 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 400 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 354 | k-next from:body -> { | 401 | k-next from:body -> { | ||
| 355 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 402 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 356 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 403 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/blob-too-large.html +162 -114
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>big.txt · files · example.test</title> | 7 | <title>big.txt · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,45 +42,62 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">main</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 68 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 72 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work has-rail"> | 86 | <div class="work has-rail"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <h1 class="file-h"> | 88 | <h1 class="file-h"> | ||
| 66 | <span>big.txt</span> | 89 | <span>big.txt</span> | ||
| 67 | <span class="acts"> | 90 | <span class="acts"> | ||
| 68 | - | <a href="/files/raw/main/big.txt">raw</a> | 91 | + | <a class="btn" href="/files/raw/main/big.txt">Raw</a> |
| 69 | - | <a href="/files/commits/main/big.txt">history</a> | 92 | + | <a class="btn" href="/files/commits/main/big.txt">History</a> |
| 70 | - | <a href="/files/tree/main">dir</a> | 93 | + | <a class="btn" href="/files/tree/main">Folder</a> |
| 71 | </span> | 94 | </span> | ||
| 72 | </h1> | 95 | </h1> | ||
| 73 | <p class="notice" role="note">file too large to show (1.1 MiB, the limit is 1.0 MiB). use raw.</p> | 96 | <p class="notice" role="note">file too large to show (1.1 MiB, the limit is 1.0 MiB). use raw.</p> | ||
| 74 | </main> | 97 | </main> | ||
| 75 | <aside class="rail" aria-label="Context"> | 98 | <aside class="rail" aria-label="Context"> | ||
| 76 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 77 | - | <summary>file · last change · selection</summary> | |||
| 78 | <section aria-labelledby="r-file"> | 99 | <section aria-labelledby="r-file"> | ||
| 79 | - | <h2 id="r-file">file</h2> | 100 | + | <h2 id="r-file">File</h2> |
| 80 | <dl class="kv"> | 101 | <dl class="kv"> | ||
| 81 | <dt>size</dt> | 102 | <dt>size</dt> | ||
| 82 | <dd>1.1 MiB</dd> | 103 | <dd>1.1 MiB</dd> | ||
| @@ -85,80 +106,80 @@ | |||||
| 85 | </dl> | 106 | </dl> | ||
| 86 | </section> | 107 | </section> | ||
| 87 | <section aria-labelledby="r-last"> | 108 | <section aria-labelledby="r-last"> | ||
| 88 | - | <h2 id="r-last">last change <a href="/files/commits/main/big.txt">history</a> | 109 | + | <h2 id="r-last">Last change <a href="/files/commits/main/big.txt">History</a> |
| 89 | </h2> | 110 | </h2> | ||
| 90 | <p> | 111 | <p> | ||
| 91 | <a href="/files/commit/SHA40">SHA7</a> add big.txt</p> | 112 | <a href="/files/commit/SHA40">SHA7</a> add big.txt</p> | ||
| 92 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | 113 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | ||
| 93 | </p> | 114 | </p> | ||
| 94 | </section> | 115 | </section> | ||
| 95 | - | </details> | |||
| 96 | </aside> | 116 | </aside> | ||
| 97 | </div> | 117 | </div> | ||
| 98 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 118 | + | <footer class="foot"> |
| 119 | + | <span>ermite devel</span> | |||
| 120 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 121 | + | <span class="foot-keys"> | |||
| 122 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 123 | + | </footer> | |||
| 124 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 125 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 126 | + | </span> | |||
| 127 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 128 | + | </span> | |||
| 129 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 130 | + | </span> | |||
| 131 | + | </div> | |||
| 132 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 99 | <div class="panel-head"> | 133 | <div class="panel-head"> | ||
| 100 | - | <span>keys</span> | 134 | + | <h2>Keyboard shortcuts</h2> |
| 101 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 135 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 136 | + | <kbd>esc</kbd> close</button> | |||
| 102 | </div> | 137 | </div> | ||
| 103 | <dl class="keys"> | 138 | <dl class="keys"> | ||
| 104 | <div> | 139 | <div> | ||
| 105 | <dt> | 140 | <dt> | ||
| 106 | - | <kbd>j</kbd> | 141 | + | <kbd>ctrl k</kbd> |
| 107 | - | <kbd>k</kbd> | 142 | + | <kbd>:</kbd> |
| 108 | </dt> | 143 | </dt> | ||
| 109 | - | <dd>move cursor down / up</dd> | 144 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 110 | </div> | 145 | </div> | ||
| 111 | <div> | 146 | <div> | ||
| 112 | <dt> | 147 | <dt> | ||
| 113 | - | <kbd>enter</kbd> | 148 | + | <kbd>?</kbd> |
| 114 | </dt> | 149 | </dt> | ||
| 115 | - | <dd>open row under cursor</dd> | 150 | + | <dd>this list</dd> |
| 116 | </div> | 151 | </div> | ||
| 117 | <div> | 152 | <div> | ||
| 118 | <dt> | 153 | <dt> | ||
| 119 | - | <kbd>g</kbd> | 154 | + | <kbd>j</kbd> |
| 120 | - | <kbd>g</kbd> / <kbd>G</kbd> | 155 | + | <kbd>k</kbd> |
| 121 | </dt> | 156 | </dt> | ||
| 122 | - | <dd>first / last row</dd> | 157 | + | <dd>move the row cursor down / up</dd> |
| 123 | </div> | 158 | </div> | ||
| 124 | <div> | 159 | <div> | ||
| 125 | <dt> | 160 | <dt> | ||
| 126 | - | <kbd>/</kbd> | 161 | + | <kbd>enter</kbd> |
| 127 | </dt> | 162 | </dt> | ||
| 128 | - | <dd>focus the filter</dd> | 163 | + | <dd>open the row under the cursor</dd> |
| 129 | </div> | 164 | </div> | ||
| 130 | <div> | 165 | <div> | ||
| 131 | <dt> | 166 | <dt> | ||
| 132 | - | <kbd>g h</kbd> | 167 | + | <kbd>g g</kbd> |
| 133 | - | <kbd>g t</kbd> | 168 | + | <kbd>G</kbd> |
| 134 | </dt> | 169 | </dt> | ||
| 135 | - | <dd>go home / tree</dd> | 170 | + | <dd>first / last row</dd> |
| 136 | </div> | 171 | </div> | ||
| 137 | <div> | 172 | <div> | ||
| 138 | <dt> | 173 | <dt> | ||
| 139 | - | <kbd>g c</kbd> | 174 | + | <kbd>/</kbd> |
| 140 | - | <kbd>g r</kbd> | |||
| 141 | </dt> | 175 | </dt> | ||
| 142 | - | <dd>go commits / refs</dd> | 176 | + | <dd>filter the list</dd> |
| 143 | </div> | 177 | </div> | ||
| 144 | <div> | 178 | <div> | ||
| 145 | <dt> | 179 | <dt> | ||
| 146 | <kbd>b</kbd> | 180 | <kbd>b</kbd> | ||
| 147 | </dt> | 181 | </dt> | ||
| 148 | - | <dd>switch ref (branches, tags)</dd> | 182 | + | <dd>switch branch or tag</dd> |
| 149 | - | </div> | |||
| 150 | - | <div> | |||
| 151 | - | <dt> | |||
| 152 | - | <kbd>:</kbd> | |||
| 153 | - | <kbd>f</kbd> | |||
| 154 | - | </dt> | |||
| 155 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 156 | - | </div> | |||
| 157 | - | <div> | |||
| 158 | - | <dt> | |||
| 159 | - | <kbd>y</kbd> | |||
| 160 | - | </dt> | |||
| 161 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 162 | </div> | 183 | </div> | ||
| 163 | <div> | 184 | <div> | ||
| 164 | <dt> | 185 | <dt> | ||
| @@ -166,15 +187,21 @@ | |||||
| 166 | <kbd>t</kbd> | 187 | <kbd>t</kbd> | ||
| 167 | <kbd>c</kbd> | 188 | <kbd>c</kbd> | ||
| 168 | <kbd>r</kbd> | 189 | <kbd>r</kbd> | ||
| 190 | + | <kbd>m</kbd> | |||
| 169 | </dt> | 191 | </dt> | ||
| 170 | - | <dd>repo sections (when shown)</dd> | 192 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 171 | </div> | 193 | </div> | ||
| 172 | <div> | 194 | <div> | ||
| 173 | <dt> | 195 | <dt> | ||
| 174 | - | <kbd>m</kbd> | 196 | + | <kbd>g h</kbd> |
| 175 | - | <kbd>g m</kbd> | 197 | + | </dt> |
| 198 | + | <dd>home</dd> | |||
| 199 | + | </div> | |||
| 200 | + | <div> | |||
| 201 | + | <dt> | |||
| 202 | + | <kbd>y</kbd> | |||
| 176 | </dt> | 203 | </dt> | ||
| 177 | - | <dd>merge requests (when shown)</dd> | 204 | + | <dd>copy a permalink pinned to the commit</dd> |
| 178 | </div> | 205 | </div> | ||
| 179 | <div> | 206 | <div> | ||
| 180 | <dt> | 207 | <dt> | ||
| @@ -194,19 +221,21 @@ | |||||
| 194 | <dt> | 221 | <dt> | ||
| 195 | <kbd>esc</kbd> | 222 | <kbd>esc</kbd> | ||
| 196 | </dt> | 223 | </dt> | ||
| 197 | - | <dd>close panel</dd> | 224 | + | <dd>close a dialog, leave a field</dd> |
| 198 | </div> | 225 | </div> | ||
| 199 | </dl> | 226 | </dl> | ||
| 200 | </dialog> | 227 | </dialog> | ||
| 201 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 228 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 202 | <div class="panel-head"> | 229 | <div class="panel-head"> | ||
| 203 | - | <label for="refs-filter">ref></label> | 230 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 204 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 231 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 205 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 232 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 233 | + | <kbd>esc</kbd> | |||
| 234 | + | </button> | |||
| 206 | </div> | 235 | </div> | ||
| 207 | <div class="panel-cols" data-picklist> | 236 | <div class="panel-cols" data-picklist> | ||
| 208 | <section aria-label="Branches"> | 237 | <section aria-label="Branches"> | ||
| 209 | - | <h2>branches <span class="mute">2</span> | 238 | + | <h2>Branches <span class="count">2</span> |
| 210 | </h2> | 239 | </h2> | ||
| 211 | <ul> | 240 | <ul> | ||
| 212 | <li> | 241 | <li> | ||
| @@ -220,7 +249,7 @@ | |||||
| 220 | </ul> | 249 | </ul> | ||
| 221 | </section> | 250 | </section> | ||
| 222 | <section aria-label="Tags"> | 251 | <section aria-label="Tags"> | ||
| 223 | - | <h2>tags <span class="mute">1</span> | 252 | + | <h2>Tags <span class="count">1</span> |
| 224 | </h2> | 253 | </h2> | ||
| 225 | <ul> | 254 | <ul> | ||
| 226 | <li> | 255 | <li> | ||
| @@ -231,105 +260,124 @@ | |||||
| 231 | </section> | 260 | </section> | ||
| 232 | </div> | 261 | </div> | ||
| 233 | </dialog> | 262 | </dialog> | ||
| 234 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 263 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 235 | <div class="panel-head"> | 264 | <div class="panel-head"> | ||
| 236 | - | <label for="cmd-q" class="hi">:</label> | 265 | + | <label for="cmd-q" class="sr">go to</label> |
| 237 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 266 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 238 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 267 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 239 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 268 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 240 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 269 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 241 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 270 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 242 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 271 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 243 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 272 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 244 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 273 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 274 | + | <kbd>esc</kbd> | |||
| 275 | + | </button> | |||
| 245 | </div> | 276 | </div> | ||
| 246 | - | <ul id="cmd-list" class="cmd-list"> | 277 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 247 | <li data-k="go home" data-cursor> | 278 | <li data-k="go home" data-cursor> | ||
| 248 | <a href="/" tabindex="-1"> | 279 | <a href="/" tabindex="-1"> | ||
| 249 | - | <span class="k">go</span>home</a> | 280 | + | <span class="lbl">home</span> |
| 281 | + | <span class="k">go</span> | |||
| 282 | + | </a> | |||
| 250 | </li> | 283 | </li> | ||
| 251 | <li data-k="section summary"> | 284 | <li data-k="section summary"> | ||
| 252 | <a href="/files" tabindex="-1"> | 285 | <a href="/files" tabindex="-1"> | ||
| 253 | - | <span class="k">section</span>summary</a> | 286 | + | <span class="lbl">summary</span> |
| 287 | + | <span class="k">section</span> | |||
| 288 | + | </a> | |||
| 254 | </li> | 289 | </li> | ||
| 255 | - | <li data-k="section tree"> | 290 | + | <li data-k="section files"> |
| 256 | <a href="/files/tree/main" tabindex="-1"> | 291 | <a href="/files/tree/main" tabindex="-1"> | ||
| 257 | - | <span class="k">section</span>tree</a> | 292 | + | <span class="lbl">files</span> |
| 293 | + | <span class="k">section</span> | |||
| 294 | + | </a> | |||
| 258 | </li> | 295 | </li> | ||
| 259 | <li data-k="section commits"> | 296 | <li data-k="section commits"> | ||
| 260 | <a href="/files/commits/main" tabindex="-1"> | 297 | <a href="/files/commits/main" tabindex="-1"> | ||
| 261 | - | <span class="k">section</span>commits</a> | 298 | + | <span class="lbl">commits</span> |
| 299 | + | <span class="k">section</span> | |||
| 300 | + | </a> | |||
| 262 | </li> | 301 | </li> | ||
| 263 | - | <li data-k="section refs"> | 302 | + | <li data-k="section branches & tags"> |
| 264 | <a href="/files/refs" tabindex="-1"> | 303 | <a href="/files/refs" tabindex="-1"> | ||
| 265 | - | <span class="k">section</span>refs</a> | 304 | + | <span class="lbl">branches & tags</span> |
| 305 | + | <span class="k">section</span> | |||
| 306 | + | </a> | |||
| 266 | </li> | 307 | </li> | ||
| 267 | <li data-k="section merge requests"> | 308 | <li data-k="section merge requests"> | ||
| 268 | <a href="/files/merge-requests" tabindex="-1"> | 309 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 269 | - | <span class="k">section</span>merge requests</a> | 310 | + | <span class="lbl">merge requests</span> |
| 311 | + | <span class="k">section</span> | |||
| 312 | + | </a> | |||
| 270 | </li> | 313 | </li> | ||
| 271 | <li data-k="branch feature/x"> | 314 | <li data-k="branch feature/x"> | ||
| 272 | <a href="/files/tree/feature/x" tabindex="-1"> | 315 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 273 | - | <span class="k">branch</span>feature/x</a> | 316 | + | <span class="lbl">feature/x</span> |
| 317 | + | <span class="k">branch</span> | |||
| 318 | + | </a> | |||
| 274 | </li> | 319 | </li> | ||
| 275 | <li data-k="branch main"> | 320 | <li data-k="branch main"> | ||
| 276 | <a href="/files/tree/main" tabindex="-1"> | 321 | <a href="/files/tree/main" tabindex="-1"> | ||
| 277 | - | <span class="k">branch</span>main</a> | 322 | + | <span class="lbl">main</span> |
| 323 | + | <span class="k">branch</span> | |||
| 324 | + | </a> | |||
| 278 | </li> | 325 | </li> | ||
| 279 | <li data-k="tag v1.0"> | 326 | <li data-k="tag v1.0"> | ||
| 280 | <a href="/files/tree/v1.0" tabindex="-1"> | 327 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 281 | - | <span class="k">tag</span>v1.0</a> | 328 | + | <span class="lbl">v1.0</span> |
| 329 | + | <span class="k">tag</span> | |||
| 330 | + | </a> | |||
| 282 | </li> | 331 | </li> | ||
| 283 | </ul> | 332 | </ul> | ||
| 333 | + | <p class="panel-foot"> | |||
| 334 | + | <span> | |||
| 335 | + | <kbd>↑</kbd> | |||
| 336 | + | <kbd>↓</kbd> move</span> | |||
| 337 | + | <span> | |||
| 338 | + | <kbd>enter</kbd> open</span> | |||
| 339 | + | <span> | |||
| 340 | + | <kbd>esc</kbd> close</span> | |||
| 341 | + | </p> | |||
| 284 | </dialog> | 342 | </dialog> | ||
| 285 | - | <footer class="bar status" aria-label="Status"> | 343 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 286 | - | <span class="mode">blob</span> | |||
| 287 | - | <span class="st-ctx">big.txt</span> | |||
| 288 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 289 | - | <span class="hi">:</span> go to…</button> | |||
| 290 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 291 | - | </span> | |||
| 292 | - | <span class="st-hints">click a line number · y permalink · ? help</span> | |||
| 293 | - | <span class="st-src"> | |||
| 294 | - | <span class="st-ver">ermite devel · </span> | |||
| 295 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 296 | - | </span> | |||
| 297 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 298 | - | </footer> | |||
| 299 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 300 | if (q('dialog[open]').count) return; | 344 | if (q('dialog[open]').count) return; | ||
| 301 | let rows = q('[data-rows] > :not([hidden])').arr(); | 345 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 302 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 346 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 303 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 347 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 304 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 348 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 305 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 349 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 350 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 351 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 306 | })(1); | 352 | })(1); | ||
| 307 | - | k-k from:body -> ((d) => { | 353 | + | k-k from:body -> (async (d) => { |
| 308 | if (q('dialog[open]').count) return; | 354 | if (q('dialog[open]').count) return; | ||
| 309 | let rows = q('[data-rows] > :not([hidden])').arr(); | 355 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 310 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 356 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 311 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 357 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 312 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 358 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 313 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 359 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 360 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 361 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 314 | })(-1); | 362 | })(-1); | ||
| 315 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 363 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 316 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 364 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 317 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 365 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 318 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 366 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 319 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 367 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 320 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 368 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 321 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 369 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 322 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 370 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 323 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 371 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 324 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 372 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 325 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 373 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 326 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 374 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 327 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 375 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 328 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 376 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 329 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 377 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 330 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 378 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 331 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 379 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 332 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 380 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 333 | k-next from:body -> { | 381 | k-next from:body -> { | ||
| 334 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 382 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 335 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 383 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/blob-trojan.html +163 -116
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>evil.go · files · example.test</title> | 7 | <title>evil.go · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,36 +42,55 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">main</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 68 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 72 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work has-rail"> | 86 | <div class="work has-rail"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <h1 class="file-h"> | 88 | <h1 class="file-h"> | ||
| 66 | <span>evil.go</span> | 89 | <span>evil.go</span> | ||
| 67 | <span class="acts"> | 90 | <span class="acts"> | ||
| 68 | - | <a href="/files/raw/main/evil.go">raw</a> | 91 | + | <a class="btn" href="/files/raw/main/evil.go">Raw</a> |
| 69 | - | <a href="/files/commits/main/evil.go">history</a> | 92 | + | <a class="btn" href="/files/commits/main/evil.go">History</a> |
| 70 | - | <a href="/files/tree/main">dir</a> | 93 | + | <a class="btn" href="/files/tree/main">Folder</a> |
| 71 | </span> | 94 | </span> | ||
| 72 | </h1> | 95 | </h1> | ||
| 73 | <pre class="chroma code" data-anchor="" hx-on:click="let a = event.target.closest('a.ln'); if (!a) return; event.preventDefault(); | 96 | <pre class="chroma code" data-anchor="" hx-on:click="let a = event.target.closest('a.ln'); if (!a) return; event.preventDefault(); | ||
| @@ -133,10 +156,8 @@ | |||||
| 133 | </pre> | 156 | </pre> | ||
| 134 | </main> | 157 | </main> | ||
| 135 | <aside class="rail" aria-label="Context"> | 158 | <aside class="rail" aria-label="Context"> | ||
| 136 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 137 | - | <summary>file · last change · selection</summary> | |||
| 138 | <section aria-labelledby="r-file"> | 159 | <section aria-labelledby="r-file"> | ||
| 139 | - | <h2 id="r-file">file</h2> | 160 | + | <h2 id="r-file">File</h2> |
| 140 | <dl class="kv"> | 161 | <dl class="kv"> | ||
| 141 | <dt>size</dt> | 162 | <dt>size</dt> | ||
| 142 | <dd>87 B</dd> | 163 | <dd>87 B</dd> | ||
| @@ -147,7 +168,7 @@ | |||||
| 147 | </dl> | 168 | </dl> | ||
| 148 | </section> | 169 | </section> | ||
| 149 | <section aria-labelledby="r-last"> | 170 | <section aria-labelledby="r-last"> | ||
| 150 | - | <h2 id="r-last">last change <a href="/files/commits/main/evil.go">history</a> | 171 | + | <h2 id="r-last">Last change <a href="/files/commits/main/evil.go">History</a> |
| 151 | </h2> | 172 | </h2> | ||
| 152 | <p> | 173 | <p> | ||
| 153 | <a href="/files/commit/SHA40">SHA7</a> add evil.go</p> | 174 | <a href="/files/commit/SHA40">SHA7</a> add evil.go</p> | ||
| @@ -155,77 +176,77 @@ | |||||
| 155 | </p> | 176 | </p> | ||
| 156 | </section> | 177 | </section> | ||
| 157 | <section aria-labelledby="r-sel"> | 178 | <section aria-labelledby="r-sel"> | ||
| 158 | - | <h2 id="r-sel">selection</h2> | 179 | + | <h2 id="r-sel">Selection</h2> |
| 159 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | 180 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | ||
| 160 | <p class="dim">y copies a permalink pinned to the commit sha</p> | 181 | <p class="dim">y copies a permalink pinned to the commit sha</p> | ||
| 161 | </section> | 182 | </section> | ||
| 162 | - | </details> | |||
| 163 | </aside> | 183 | </aside> | ||
| 164 | </div> | 184 | </div> | ||
| 165 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 185 | + | <footer class="foot"> |
| 186 | + | <span>ermite devel</span> | |||
| 187 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 188 | + | <span class="foot-keys"> | |||
| 189 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 190 | + | </footer> | |||
| 191 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 192 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 193 | + | </span> | |||
| 194 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 195 | + | </span> | |||
| 196 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 197 | + | </span> | |||
| 198 | + | </div> | |||
| 199 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 166 | <div class="panel-head"> | 200 | <div class="panel-head"> | ||
| 167 | - | <span>keys</span> | 201 | + | <h2>Keyboard shortcuts</h2> |
| 168 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 202 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 203 | + | <kbd>esc</kbd> close</button> | |||
| 169 | </div> | 204 | </div> | ||
| 170 | <dl class="keys"> | 205 | <dl class="keys"> | ||
| 171 | <div> | 206 | <div> | ||
| 172 | <dt> | 207 | <dt> | ||
| 173 | - | <kbd>j</kbd> | 208 | + | <kbd>ctrl k</kbd> |
| 174 | - | <kbd>k</kbd> | 209 | + | <kbd>:</kbd> |
| 175 | </dt> | 210 | </dt> | ||
| 176 | - | <dd>move cursor down / up</dd> | 211 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 177 | </div> | 212 | </div> | ||
| 178 | <div> | 213 | <div> | ||
| 179 | <dt> | 214 | <dt> | ||
| 180 | - | <kbd>enter</kbd> | 215 | + | <kbd>?</kbd> |
| 181 | </dt> | 216 | </dt> | ||
| 182 | - | <dd>open row under cursor</dd> | 217 | + | <dd>this list</dd> |
| 183 | </div> | 218 | </div> | ||
| 184 | <div> | 219 | <div> | ||
| 185 | <dt> | 220 | <dt> | ||
| 186 | - | <kbd>g</kbd> | 221 | + | <kbd>j</kbd> |
| 187 | - | <kbd>g</kbd> / <kbd>G</kbd> | 222 | + | <kbd>k</kbd> |
| 188 | </dt> | 223 | </dt> | ||
| 189 | - | <dd>first / last row</dd> | 224 | + | <dd>move the row cursor down / up</dd> |
| 190 | </div> | 225 | </div> | ||
| 191 | <div> | 226 | <div> | ||
| 192 | <dt> | 227 | <dt> | ||
| 193 | - | <kbd>/</kbd> | 228 | + | <kbd>enter</kbd> |
| 194 | </dt> | 229 | </dt> | ||
| 195 | - | <dd>focus the filter</dd> | 230 | + | <dd>open the row under the cursor</dd> |
| 196 | </div> | 231 | </div> | ||
| 197 | <div> | 232 | <div> | ||
| 198 | <dt> | 233 | <dt> | ||
| 199 | - | <kbd>g h</kbd> | 234 | + | <kbd>g g</kbd> |
| 200 | - | <kbd>g t</kbd> | 235 | + | <kbd>G</kbd> |
| 201 | </dt> | 236 | </dt> | ||
| 202 | - | <dd>go home / tree</dd> | 237 | + | <dd>first / last row</dd> |
| 203 | </div> | 238 | </div> | ||
| 204 | <div> | 239 | <div> | ||
| 205 | <dt> | 240 | <dt> | ||
| 206 | - | <kbd>g c</kbd> | 241 | + | <kbd>/</kbd> |
| 207 | - | <kbd>g r</kbd> | |||
| 208 | </dt> | 242 | </dt> | ||
| 209 | - | <dd>go commits / refs</dd> | 243 | + | <dd>filter the list</dd> |
| 210 | </div> | 244 | </div> | ||
| 211 | <div> | 245 | <div> | ||
| 212 | <dt> | 246 | <dt> | ||
| 213 | <kbd>b</kbd> | 247 | <kbd>b</kbd> | ||
| 214 | </dt> | 248 | </dt> | ||
| 215 | - | <dd>switch ref (branches, tags)</dd> | 249 | + | <dd>switch branch or tag</dd> |
| 216 | - | </div> | |||
| 217 | - | <div> | |||
| 218 | - | <dt> | |||
| 219 | - | <kbd>:</kbd> | |||
| 220 | - | <kbd>f</kbd> | |||
| 221 | - | </dt> | |||
| 222 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 223 | - | </div> | |||
| 224 | - | <div> | |||
| 225 | - | <dt> | |||
| 226 | - | <kbd>y</kbd> | |||
| 227 | - | </dt> | |||
| 228 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 229 | </div> | 250 | </div> | ||
| 230 | <div> | 251 | <div> | ||
| 231 | <dt> | 252 | <dt> | ||
| @@ -233,15 +254,21 @@ | |||||
| 233 | <kbd>t</kbd> | 254 | <kbd>t</kbd> | ||
| 234 | <kbd>c</kbd> | 255 | <kbd>c</kbd> | ||
| 235 | <kbd>r</kbd> | 256 | <kbd>r</kbd> | ||
| 257 | + | <kbd>m</kbd> | |||
| 236 | </dt> | 258 | </dt> | ||
| 237 | - | <dd>repo sections (when shown)</dd> | 259 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 238 | </div> | 260 | </div> | ||
| 239 | <div> | 261 | <div> | ||
| 240 | <dt> | 262 | <dt> | ||
| 241 | - | <kbd>m</kbd> | 263 | + | <kbd>g h</kbd> |
| 242 | - | <kbd>g m</kbd> | 264 | + | </dt> |
| 265 | + | <dd>home</dd> | |||
| 266 | + | </div> | |||
| 267 | + | <div> | |||
| 268 | + | <dt> | |||
| 269 | + | <kbd>y</kbd> | |||
| 243 | </dt> | 270 | </dt> | ||
| 244 | - | <dd>merge requests (when shown)</dd> | 271 | + | <dd>copy a permalink pinned to the commit</dd> |
| 245 | </div> | 272 | </div> | ||
| 246 | <div> | 273 | <div> | ||
| 247 | <dt> | 274 | <dt> | ||
| @@ -261,19 +288,21 @@ | |||||
| 261 | <dt> | 288 | <dt> | ||
| 262 | <kbd>esc</kbd> | 289 | <kbd>esc</kbd> | ||
| 263 | </dt> | 290 | </dt> | ||
| 264 | - | <dd>close panel</dd> | 291 | + | <dd>close a dialog, leave a field</dd> |
| 265 | </div> | 292 | </div> | ||
| 266 | </dl> | 293 | </dl> | ||
| 267 | </dialog> | 294 | </dialog> | ||
| 268 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 295 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 269 | <div class="panel-head"> | 296 | <div class="panel-head"> | ||
| 270 | - | <label for="refs-filter">ref></label> | 297 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 271 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 298 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 272 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 299 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 300 | + | <kbd>esc</kbd> | |||
| 301 | + | </button> | |||
| 273 | </div> | 302 | </div> | ||
| 274 | <div class="panel-cols" data-picklist> | 303 | <div class="panel-cols" data-picklist> | ||
| 275 | <section aria-label="Branches"> | 304 | <section aria-label="Branches"> | ||
| 276 | - | <h2>branches <span class="mute">2</span> | 305 | + | <h2>Branches <span class="count">2</span> |
| 277 | </h2> | 306 | </h2> | ||
| 278 | <ul> | 307 | <ul> | ||
| 279 | <li> | 308 | <li> | ||
| @@ -287,7 +316,7 @@ | |||||
| 287 | </ul> | 316 | </ul> | ||
| 288 | </section> | 317 | </section> | ||
| 289 | <section aria-label="Tags"> | 318 | <section aria-label="Tags"> | ||
| 290 | - | <h2>tags <span class="mute">1</span> | 319 | + | <h2>Tags <span class="count">1</span> |
| 291 | </h2> | 320 | </h2> | ||
| 292 | <ul> | 321 | <ul> | ||
| 293 | <li> | 322 | <li> | ||
| @@ -298,106 +327,124 @@ | |||||
| 298 | </section> | 327 | </section> | ||
| 299 | </div> | 328 | </div> | ||
| 300 | </dialog> | 329 | </dialog> | ||
| 301 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 330 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 302 | <div class="panel-head"> | 331 | <div class="panel-head"> | ||
| 303 | - | <label for="cmd-q" class="hi">:</label> | 332 | + | <label for="cmd-q" class="sr">go to</label> |
| 304 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 333 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 305 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 334 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 306 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 335 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 307 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 336 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 308 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 337 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 309 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 338 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 310 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 339 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 311 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 340 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 341 | + | <kbd>esc</kbd> | |||
| 342 | + | </button> | |||
| 312 | </div> | 343 | </div> | ||
| 313 | - | <ul id="cmd-list" class="cmd-list"> | 344 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 314 | <li data-k="go home" data-cursor> | 345 | <li data-k="go home" data-cursor> | ||
| 315 | <a href="/" tabindex="-1"> | 346 | <a href="/" tabindex="-1"> | ||
| 316 | - | <span class="k">go</span>home</a> | 347 | + | <span class="lbl">home</span> |
| 348 | + | <span class="k">go</span> | |||
| 349 | + | </a> | |||
| 317 | </li> | 350 | </li> | ||
| 318 | <li data-k="section summary"> | 351 | <li data-k="section summary"> | ||
| 319 | <a href="/files" tabindex="-1"> | 352 | <a href="/files" tabindex="-1"> | ||
| 320 | - | <span class="k">section</span>summary</a> | 353 | + | <span class="lbl">summary</span> |
| 354 | + | <span class="k">section</span> | |||
| 355 | + | </a> | |||
| 321 | </li> | 356 | </li> | ||
| 322 | - | <li data-k="section tree"> | 357 | + | <li data-k="section files"> |
| 323 | <a href="/files/tree/main" tabindex="-1"> | 358 | <a href="/files/tree/main" tabindex="-1"> | ||
| 324 | - | <span class="k">section</span>tree</a> | 359 | + | <span class="lbl">files</span> |
| 360 | + | <span class="k">section</span> | |||
| 361 | + | </a> | |||
| 325 | </li> | 362 | </li> | ||
| 326 | <li data-k="section commits"> | 363 | <li data-k="section commits"> | ||
| 327 | <a href="/files/commits/main" tabindex="-1"> | 364 | <a href="/files/commits/main" tabindex="-1"> | ||
| 328 | - | <span class="k">section</span>commits</a> | 365 | + | <span class="lbl">commits</span> |
| 366 | + | <span class="k">section</span> | |||
| 367 | + | </a> | |||
| 329 | </li> | 368 | </li> | ||
| 330 | - | <li data-k="section refs"> | 369 | + | <li data-k="section branches & tags"> |
| 331 | <a href="/files/refs" tabindex="-1"> | 370 | <a href="/files/refs" tabindex="-1"> | ||
| 332 | - | <span class="k">section</span>refs</a> | 371 | + | <span class="lbl">branches & tags</span> |
| 372 | + | <span class="k">section</span> | |||
| 373 | + | </a> | |||
| 333 | </li> | 374 | </li> | ||
| 334 | <li data-k="section merge requests"> | 375 | <li data-k="section merge requests"> | ||
| 335 | <a href="/files/merge-requests" tabindex="-1"> | 376 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 336 | - | <span class="k">section</span>merge requests</a> | 377 | + | <span class="lbl">merge requests</span> |
| 378 | + | <span class="k">section</span> | |||
| 379 | + | </a> | |||
| 337 | </li> | 380 | </li> | ||
| 338 | <li data-k="branch feature/x"> | 381 | <li data-k="branch feature/x"> | ||
| 339 | <a href="/files/tree/feature/x" tabindex="-1"> | 382 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 340 | - | <span class="k">branch</span>feature/x</a> | 383 | + | <span class="lbl">feature/x</span> |
| 384 | + | <span class="k">branch</span> | |||
| 385 | + | </a> | |||
| 341 | </li> | 386 | </li> | ||
| 342 | <li data-k="branch main"> | 387 | <li data-k="branch main"> | ||
| 343 | <a href="/files/tree/main" tabindex="-1"> | 388 | <a href="/files/tree/main" tabindex="-1"> | ||
| 344 | - | <span class="k">branch</span>main</a> | 389 | + | <span class="lbl">main</span> |
| 390 | + | <span class="k">branch</span> | |||
| 391 | + | </a> | |||
| 345 | </li> | 392 | </li> | ||
| 346 | <li data-k="tag v1.0"> | 393 | <li data-k="tag v1.0"> | ||
| 347 | <a href="/files/tree/v1.0" tabindex="-1"> | 394 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 348 | - | <span class="k">tag</span>v1.0</a> | 395 | + | <span class="lbl">v1.0</span> |
| 396 | + | <span class="k">tag</span> | |||
| 397 | + | </a> | |||
| 349 | </li> | 398 | </li> | ||
| 350 | </ul> | 399 | </ul> | ||
| 400 | + | <p class="panel-foot"> | |||
| 401 | + | <span> | |||
| 402 | + | <kbd>↑</kbd> | |||
| 403 | + | <kbd>↓</kbd> move</span> | |||
| 404 | + | <span> | |||
| 405 | + | <kbd>enter</kbd> open</span> | |||
| 406 | + | <span> | |||
| 407 | + | <kbd>esc</kbd> close</span> | |||
| 408 | + | </p> | |||
| 351 | </dialog> | 409 | </dialog> | ||
| 352 | - | <footer class="bar status" aria-label="Status"> | 410 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 353 | - | <span class="mode">blob</span> | |||
| 354 | - | <span class="st-ctx">evil.go</span> | |||
| 355 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 356 | - | <span class="hi">:</span> go to…</button> | |||
| 357 | - | <span class="st-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : 'click a line number · shift-click extends'">click a line number · shift-click extends</span> | |||
| 358 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 359 | - | </span> | |||
| 360 | - | <span class="st-hints">click a line number · y permalink · ? help</span> | |||
| 361 | - | <span class="st-src"> | |||
| 362 | - | <span class="st-ver">ermite devel · </span> | |||
| 363 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 364 | - | </span> | |||
| 365 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 366 | - | </footer> | |||
| 367 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 368 | if (q('dialog[open]').count) return; | 411 | if (q('dialog[open]').count) return; | ||
| 369 | let rows = q('[data-rows] > :not([hidden])').arr(); | 412 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 370 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 413 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 371 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 414 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 372 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 415 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 373 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 416 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 417 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 418 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 374 | })(1); | 419 | })(1); | ||
| 375 | - | k-k from:body -> ((d) => { | 420 | + | k-k from:body -> (async (d) => { |
| 376 | if (q('dialog[open]').count) return; | 421 | if (q('dialog[open]').count) return; | ||
| 377 | let rows = q('[data-rows] > :not([hidden])').arr(); | 422 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 378 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 423 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 379 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 424 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 380 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 425 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 381 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 426 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 427 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 428 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 382 | })(-1); | 429 | })(-1); | ||
| 383 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 430 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 384 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 431 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 385 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 432 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 386 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 433 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 387 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 434 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 388 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 435 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 389 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 436 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 390 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 437 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 391 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 438 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 392 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 439 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 393 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 440 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 394 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 441 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 395 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 442 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 396 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 443 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 397 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 444 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 398 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 445 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 399 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 446 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 400 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 447 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 401 | k-next from:body -> { | 448 | k-next from:body -> { | ||
| 402 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 449 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 403 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 450 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commit-big.html +167 -114
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>SHA7 add a very large file · diffs · example.test</title> | 7 | <title>SHA7 add a very large file · diffs · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,30 +39,49 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">detached</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/diffs" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <header class="show"> | 85 | <header class="show"> | ||
| 63 | <h1>add a very large file</h1> | 86 | <h1>add a very large file</h1> | ||
| 64 | <dl class="kv show-kv"> | 87 | <dl class="kv show-kv"> | ||
| @@ -71,7 +94,9 @@ | |||||
| 71 | <a href="/diffs/commit/SHA40">SHA7</a> | 94 | <a href="/diffs/commit/SHA40">SHA7</a> | ||
| 72 | </dd> | 95 | </dd> | ||
| 73 | <dt>refs</dt> | 96 | <dt>refs</dt> | ||
| 74 | - | <dd>[main] </dd> | 97 | + | <dd> |
| 98 | + | <span class="pill">main</span> | |||
| 99 | + | </dd> | |||
| 75 | </dl> | 100 | </dl> | ||
| 76 | </header> | 101 | </header> | ||
| 77 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | 102 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | ||
| @@ -80,9 +105,12 @@ | |||||
| 80 | </span> | 105 | </span> | ||
| 81 | <span class="grow"> | 106 | <span class="grow"> | ||
| 82 | </span> | 107 | </span> | ||
| 83 | - | <a href="/diffs/commit/SHA40?view=unified" class="vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">[u]nified</a> | 108 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">Unified<kbd>u</kbd> |
| 84 | - | <a href="/diffs/commit/SHA40?view=split" class="vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">[d] split</a> | 109 | + | </a> |
| 85 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 110 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">Split<kbd>d</kbd> |
| 111 | + | </a> | |||
| 112 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd> | |||
| 113 | + | </button> | |||
| 86 | </div> | 114 | </div> | ||
| 87 | <p class="notice">this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.</p> | 115 | <p class="notice">this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.</p> | ||
| 88 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 116 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | ||
| @@ -99,10 +127,8 @@ | |||||
| 99 | </div> | 127 | </div> | ||
| 100 | </main> | 128 | </main> | ||
| 101 | <aside class="rail" aria-label="Context"> | 129 | <aside class="rail" aria-label="Context"> | ||
| 102 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 103 | - | <summary>1 changed file</summary> | |||
| 104 | <section aria-labelledby="r-files"> | 130 | <section aria-labelledby="r-files"> | ||
| 105 | - | <h2 id="r-files">files <span>1</span> | 131 | + | <h2 id="r-files">Files <span class="count">1</span> |
| 106 | </h2> | 132 | </h2> | ||
| 107 | <ol class="files" aria-label="Changed files"> | 133 | <ol class="files" aria-label="Changed files"> | ||
| 108 | <li> | 134 | <li> | ||
| @@ -122,73 +148,73 @@ | |||||
| 122 | </li> | 148 | </li> | ||
| 123 | </ol> | 149 | </ol> | ||
| 124 | </section> | 150 | </section> | ||
| 125 | - | </details> | |||
| 126 | </aside> | 151 | </aside> | ||
| 127 | </div> | 152 | </div> | ||
| 128 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 153 | + | <footer class="foot"> |
| 154 | + | <span>ermite devel</span> | |||
| 155 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 156 | + | <span class="foot-keys"> | |||
| 157 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 158 | + | </footer> | |||
| 159 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 160 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 161 | + | </span> | |||
| 162 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 163 | + | </span> | |||
| 164 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 165 | + | </span> | |||
| 166 | + | </div> | |||
| 167 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 129 | <div class="panel-head"> | 168 | <div class="panel-head"> | ||
| 130 | - | <span>keys</span> | 169 | + | <h2>Keyboard shortcuts</h2> |
| 131 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 170 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 171 | + | <kbd>esc</kbd> close</button> | |||
| 132 | </div> | 172 | </div> | ||
| 133 | <dl class="keys"> | 173 | <dl class="keys"> | ||
| 134 | <div> | 174 | <div> | ||
| 135 | <dt> | 175 | <dt> | ||
| 136 | - | <kbd>j</kbd> | 176 | + | <kbd>ctrl k</kbd> |
| 137 | - | <kbd>k</kbd> | 177 | + | <kbd>:</kbd> |
| 138 | </dt> | 178 | </dt> | ||
| 139 | - | <dd>move cursor down / up</dd> | 179 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 140 | </div> | 180 | </div> | ||
| 141 | <div> | 181 | <div> | ||
| 142 | <dt> | 182 | <dt> | ||
| 143 | - | <kbd>enter</kbd> | 183 | + | <kbd>?</kbd> |
| 144 | </dt> | 184 | </dt> | ||
| 145 | - | <dd>open row under cursor</dd> | 185 | + | <dd>this list</dd> |
| 146 | </div> | 186 | </div> | ||
| 147 | <div> | 187 | <div> | ||
| 148 | <dt> | 188 | <dt> | ||
| 149 | - | <kbd>g</kbd> | 189 | + | <kbd>j</kbd> |
| 150 | - | <kbd>g</kbd> / <kbd>G</kbd> | 190 | + | <kbd>k</kbd> |
| 151 | </dt> | 191 | </dt> | ||
| 152 | - | <dd>first / last row</dd> | 192 | + | <dd>move the row cursor down / up</dd> |
| 153 | </div> | 193 | </div> | ||
| 154 | <div> | 194 | <div> | ||
| 155 | <dt> | 195 | <dt> | ||
| 156 | - | <kbd>/</kbd> | 196 | + | <kbd>enter</kbd> |
| 157 | </dt> | 197 | </dt> | ||
| 158 | - | <dd>focus the filter</dd> | 198 | + | <dd>open the row under the cursor</dd> |
| 159 | </div> | 199 | </div> | ||
| 160 | <div> | 200 | <div> | ||
| 161 | <dt> | 201 | <dt> | ||
| 162 | - | <kbd>g h</kbd> | 202 | + | <kbd>g g</kbd> |
| 163 | - | <kbd>g t</kbd> | 203 | + | <kbd>G</kbd> |
| 164 | </dt> | 204 | </dt> | ||
| 165 | - | <dd>go home / tree</dd> | 205 | + | <dd>first / last row</dd> |
| 166 | </div> | 206 | </div> | ||
| 167 | <div> | 207 | <div> | ||
| 168 | <dt> | 208 | <dt> | ||
| 169 | - | <kbd>g c</kbd> | 209 | + | <kbd>/</kbd> |
| 170 | - | <kbd>g r</kbd> | |||
| 171 | </dt> | 210 | </dt> | ||
| 172 | - | <dd>go commits / refs</dd> | 211 | + | <dd>filter the list</dd> |
| 173 | </div> | 212 | </div> | ||
| 174 | <div> | 213 | <div> | ||
| 175 | <dt> | 214 | <dt> | ||
| 176 | <kbd>b</kbd> | 215 | <kbd>b</kbd> | ||
| 177 | </dt> | 216 | </dt> | ||
| 178 | - | <dd>switch ref (branches, tags)</dd> | 217 | + | <dd>switch branch or tag</dd> |
| 179 | - | </div> | |||
| 180 | - | <div> | |||
| 181 | - | <dt> | |||
| 182 | - | <kbd>:</kbd> | |||
| 183 | - | <kbd>f</kbd> | |||
| 184 | - | </dt> | |||
| 185 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 186 | - | </div> | |||
| 187 | - | <div> | |||
| 188 | - | <dt> | |||
| 189 | - | <kbd>y</kbd> | |||
| 190 | - | </dt> | |||
| 191 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 192 | </div> | 218 | </div> | ||
| 193 | <div> | 219 | <div> | ||
| 194 | <dt> | 220 | <dt> | ||
| @@ -196,15 +222,21 @@ | |||||
| 196 | <kbd>t</kbd> | 222 | <kbd>t</kbd> | ||
| 197 | <kbd>c</kbd> | 223 | <kbd>c</kbd> | ||
| 198 | <kbd>r</kbd> | 224 | <kbd>r</kbd> | ||
| 225 | + | <kbd>m</kbd> | |||
| 199 | </dt> | 226 | </dt> | ||
| 200 | - | <dd>repo sections (when shown)</dd> | 227 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 201 | </div> | 228 | </div> | ||
| 202 | <div> | 229 | <div> | ||
| 203 | <dt> | 230 | <dt> | ||
| 204 | - | <kbd>m</kbd> | 231 | + | <kbd>g h</kbd> |
| 205 | - | <kbd>g m</kbd> | 232 | + | </dt> |
| 233 | + | <dd>home</dd> | |||
| 234 | + | </div> | |||
| 235 | + | <div> | |||
| 236 | + | <dt> | |||
| 237 | + | <kbd>y</kbd> | |||
| 206 | </dt> | 238 | </dt> | ||
| 207 | - | <dd>merge requests (when shown)</dd> | 239 | + | <dd>copy a permalink pinned to the commit</dd> |
| 208 | </div> | 240 | </div> | ||
| 209 | <div> | 241 | <div> | ||
| 210 | <dt> | 242 | <dt> | ||
| @@ -224,19 +256,21 @@ | |||||
| 224 | <dt> | 256 | <dt> | ||
| 225 | <kbd>esc</kbd> | 257 | <kbd>esc</kbd> | ||
| 226 | </dt> | 258 | </dt> | ||
| 227 | - | <dd>close panel</dd> | 259 | + | <dd>close a dialog, leave a field</dd> |
| 228 | </div> | 260 | </div> | ||
| 229 | </dl> | 261 | </dl> | ||
| 230 | </dialog> | 262 | </dialog> | ||
| 231 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 263 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 232 | <div class="panel-head"> | 264 | <div class="panel-head"> | ||
| 233 | - | <label for="refs-filter">ref></label> | 265 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 234 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 266 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 235 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 267 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 268 | + | <kbd>esc</kbd> | |||
| 269 | + | </button> | |||
| 236 | </div> | 270 | </div> | ||
| 237 | <div class="panel-cols" data-picklist> | 271 | <div class="panel-cols" data-picklist> | ||
| 238 | <section aria-label="Branches"> | 272 | <section aria-label="Branches"> | ||
| 239 | - | <h2>branches <span class="mute">2</span> | 273 | + | <h2>Branches <span class="count">2</span> |
| 240 | </h2> | 274 | </h2> | ||
| 241 | <ul> | 275 | <ul> | ||
| 242 | <li> | 276 | <li> | ||
| @@ -250,7 +284,7 @@ | |||||
| 250 | </ul> | 284 | </ul> | ||
| 251 | </section> | 285 | </section> | ||
| 252 | <section aria-label="Tags"> | 286 | <section aria-label="Tags"> | ||
| 253 | - | <h2>tags <span class="mute">1</span> | 287 | + | <h2>Tags <span class="count">1</span> |
| 254 | </h2> | 288 | </h2> | ||
| 255 | <ul> | 289 | <ul> | ||
| 256 | <li> | 290 | <li> | ||
| @@ -261,105 +295,124 @@ | |||||
| 261 | </section> | 295 | </section> | ||
| 262 | </div> | 296 | </div> | ||
| 263 | </dialog> | 297 | </dialog> | ||
| 264 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 298 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 265 | <div class="panel-head"> | 299 | <div class="panel-head"> | ||
| 266 | - | <label for="cmd-q" class="hi">:</label> | 300 | + | <label for="cmd-q" class="sr">go to</label> |
| 267 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 301 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 268 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 302 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 269 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 303 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 270 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 304 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 271 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 305 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 272 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 306 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 273 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 307 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 274 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 308 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 309 | + | <kbd>esc</kbd> | |||
| 310 | + | </button> | |||
| 275 | </div> | 311 | </div> | ||
| 276 | - | <ul id="cmd-list" class="cmd-list"> | 312 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 277 | <li data-k="go home" data-cursor> | 313 | <li data-k="go home" data-cursor> | ||
| 278 | <a href="/" tabindex="-1"> | 314 | <a href="/" tabindex="-1"> | ||
| 279 | - | <span class="k">go</span>home</a> | 315 | + | <span class="lbl">home</span> |
| 316 | + | <span class="k">go</span> | |||
| 317 | + | </a> | |||
| 280 | </li> | 318 | </li> | ||
| 281 | <li data-k="section summary"> | 319 | <li data-k="section summary"> | ||
| 282 | <a href="/diffs" tabindex="-1"> | 320 | <a href="/diffs" tabindex="-1"> | ||
| 283 | - | <span class="k">section</span>summary</a> | 321 | + | <span class="lbl">summary</span> |
| 322 | + | <span class="k">section</span> | |||
| 323 | + | </a> | |||
| 284 | </li> | 324 | </li> | ||
| 285 | - | <li data-k="section tree"> | 325 | + | <li data-k="section files"> |
| 286 | <a href="/diffs/tree/main" tabindex="-1"> | 326 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 287 | - | <span class="k">section</span>tree</a> | 327 | + | <span class="lbl">files</span> |
| 328 | + | <span class="k">section</span> | |||
| 329 | + | </a> | |||
| 288 | </li> | 330 | </li> | ||
| 289 | <li data-k="section commits"> | 331 | <li data-k="section commits"> | ||
| 290 | <a href="/diffs/commits/main" tabindex="-1"> | 332 | <a href="/diffs/commits/main" tabindex="-1"> | ||
| 291 | - | <span class="k">section</span>commits</a> | 333 | + | <span class="lbl">commits</span> |
| 334 | + | <span class="k">section</span> | |||
| 335 | + | </a> | |||
| 292 | </li> | 336 | </li> | ||
| 293 | - | <li data-k="section refs"> | 337 | + | <li data-k="section branches & tags"> |
| 294 | <a href="/diffs/refs" tabindex="-1"> | 338 | <a href="/diffs/refs" tabindex="-1"> | ||
| 295 | - | <span class="k">section</span>refs</a> | 339 | + | <span class="lbl">branches & tags</span> |
| 340 | + | <span class="k">section</span> | |||
| 341 | + | </a> | |||
| 296 | </li> | 342 | </li> | ||
| 297 | <li data-k="section merge requests"> | 343 | <li data-k="section merge requests"> | ||
| 298 | <a href="/diffs/merge-requests" tabindex="-1"> | 344 | <a href="/diffs/merge-requests" tabindex="-1"> | ||
| 299 | - | <span class="k">section</span>merge requests</a> | 345 | + | <span class="lbl">merge requests</span> |
| 346 | + | <span class="k">section</span> | |||
| 347 | + | </a> | |||
| 300 | </li> | 348 | </li> | ||
| 301 | <li data-k="branch main"> | 349 | <li data-k="branch main"> | ||
| 302 | <a href="/diffs/tree/main" tabindex="-1"> | 350 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 303 | - | <span class="k">branch</span>main</a> | 351 | + | <span class="lbl">main</span> |
| 352 | + | <span class="k">branch</span> | |||
| 353 | + | </a> | |||
| 304 | </li> | 354 | </li> | ||
| 305 | <li data-k="branch side"> | 355 | <li data-k="branch side"> | ||
| 306 | <a href="/diffs/tree/side" tabindex="-1"> | 356 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 307 | - | <span class="k">branch</span>side</a> | 357 | + | <span class="lbl">side</span> |
| 358 | + | <span class="k">branch</span> | |||
| 359 | + | </a> | |||
| 308 | </li> | 360 | </li> | ||
| 309 | <li data-k="tag v1.0.0"> | 361 | <li data-k="tag v1.0.0"> | ||
| 310 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 362 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 311 | - | <span class="k">tag</span>v1.0.0</a> | 363 | + | <span class="lbl">v1.0.0</span> |
| 364 | + | <span class="k">tag</span> | |||
| 365 | + | </a> | |||
| 312 | </li> | 366 | </li> | ||
| 313 | </ul> | 367 | </ul> | ||
| 368 | + | <p class="panel-foot"> | |||
| 369 | + | <span> | |||
| 370 | + | <kbd>↑</kbd> | |||
| 371 | + | <kbd>↓</kbd> move</span> | |||
| 372 | + | <span> | |||
| 373 | + | <kbd>enter</kbd> open</span> | |||
| 374 | + | <span> | |||
| 375 | + | <kbd>esc</kbd> close</span> | |||
| 376 | + | </p> | |||
| 314 | </dialog> | 377 | </dialog> | ||
| 315 | - | <footer class="bar status" aria-label="Status"> | 378 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 316 | - | <span class="mode">commit</span> | |||
| 317 | - | <span class="st-ctx">SHA7 · 1 file · +2600 -0</span> | |||
| 318 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 319 | - | <span class="hi">:</span> go to…</button> | |||
| 320 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 321 | - | </span> | |||
| 322 | - | <span class="st-hints">d split · u unified · x collapse all · ? help</span> | |||
| 323 | - | <span class="st-src"> | |||
| 324 | - | <span class="st-ver">ermite devel · </span> | |||
| 325 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 326 | - | </span> | |||
| 327 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 328 | - | </footer> | |||
| 329 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 330 | if (q('dialog[open]').count) return; | 379 | if (q('dialog[open]').count) return; | ||
| 331 | let rows = q('[data-rows] > :not([hidden])').arr(); | 380 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 332 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 381 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 333 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 382 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 334 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 383 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 335 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 384 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 385 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 386 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 336 | })(1); | 387 | })(1); | ||
| 337 | - | k-k from:body -> ((d) => { | 388 | + | k-k from:body -> (async (d) => { |
| 338 | if (q('dialog[open]').count) return; | 389 | if (q('dialog[open]').count) return; | ||
| 339 | let rows = q('[data-rows] > :not([hidden])').arr(); | 390 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 340 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 391 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 341 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 392 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 342 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 393 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 343 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 394 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 395 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 396 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 344 | })(-1); | 397 | })(-1); | ||
| 345 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 398 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 346 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 399 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 347 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 400 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 348 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 401 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 349 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 402 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 350 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 403 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 351 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 404 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 352 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 405 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 353 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 406 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 354 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 407 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 355 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 408 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 356 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 409 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 357 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 410 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 358 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 411 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 359 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 412 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 360 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 413 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 361 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 414 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 362 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 415 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 363 | k-next from:body -> { | 416 | k-next from:body -> { | ||
| 364 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 417 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 365 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 418 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commit-binary.html +164 -113
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>SHA7 change a binary file · diffs · example.test</title> | 7 | <title>SHA7 change a binary file · diffs · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,30 +39,49 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">detached</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/diffs" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <header class="show"> | 85 | <header class="show"> | ||
| 63 | <h1>change a binary file</h1> | 86 | <h1>change a binary file</h1> | ||
| 64 | <dl class="kv show-kv"> | 87 | <dl class="kv show-kv"> | ||
| @@ -78,9 +101,12 @@ | |||||
| 78 | </span> | 101 | </span> | ||
| 79 | <span class="grow"> | 102 | <span class="grow"> | ||
| 80 | </span> | 103 | </span> | ||
| 81 | - | <a href="/diffs/commit/SHA40?view=unified" class="vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">[u]nified</a> | 104 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">Unified<kbd>u</kbd> |
| 82 | - | <a href="/diffs/commit/SHA40?view=split" class="vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">[d] split</a> | 105 | + | </a> |
| 83 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 106 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">Split<kbd>d</kbd> |
| 107 | + | </a> | |||
| 108 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd> | |||
| 109 | + | </button> | |||
| 84 | </div> | 110 | </div> | ||
| 85 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 111 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | ||
| 86 | <details class="dfile" id="f1" data-file> | 112 | <details class="dfile" id="f1" data-file> | ||
| @@ -96,10 +122,8 @@ | |||||
| 96 | </div> | 122 | </div> | ||
| 97 | </main> | 123 | </main> | ||
| 98 | <aside class="rail" aria-label="Context"> | 124 | <aside class="rail" aria-label="Context"> | ||
| 99 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 100 | - | <summary>1 changed file</summary> | |||
| 101 | <section aria-labelledby="r-files"> | 125 | <section aria-labelledby="r-files"> | ||
| 102 | - | <h2 id="r-files">files <span>1</span> | 126 | + | <h2 id="r-files">Files <span class="count">1</span> |
| 103 | </h2> | 127 | </h2> | ||
| 104 | <ol class="files" aria-label="Changed files"> | 128 | <ol class="files" aria-label="Changed files"> | ||
| 105 | <li> | 129 | <li> | ||
| @@ -119,73 +143,73 @@ | |||||
| 119 | </li> | 143 | </li> | ||
| 120 | </ol> | 144 | </ol> | ||
| 121 | </section> | 145 | </section> | ||
| 122 | - | </details> | |||
| 123 | </aside> | 146 | </aside> | ||
| 124 | </div> | 147 | </div> | ||
| 125 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 148 | + | <footer class="foot"> |
| 149 | + | <span>ermite devel</span> | |||
| 150 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 151 | + | <span class="foot-keys"> | |||
| 152 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 153 | + | </footer> | |||
| 154 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 155 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 156 | + | </span> | |||
| 157 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 158 | + | </span> | |||
| 159 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 160 | + | </span> | |||
| 161 | + | </div> | |||
| 162 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 126 | <div class="panel-head"> | 163 | <div class="panel-head"> | ||
| 127 | - | <span>keys</span> | 164 | + | <h2>Keyboard shortcuts</h2> |
| 128 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 165 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 166 | + | <kbd>esc</kbd> close</button> | |||
| 129 | </div> | 167 | </div> | ||
| 130 | <dl class="keys"> | 168 | <dl class="keys"> | ||
| 131 | <div> | 169 | <div> | ||
| 132 | <dt> | 170 | <dt> | ||
| 133 | - | <kbd>j</kbd> | 171 | + | <kbd>ctrl k</kbd> |
| 134 | - | <kbd>k</kbd> | 172 | + | <kbd>:</kbd> |
| 135 | </dt> | 173 | </dt> | ||
| 136 | - | <dd>move cursor down / up</dd> | 174 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 137 | </div> | 175 | </div> | ||
| 138 | <div> | 176 | <div> | ||
| 139 | <dt> | 177 | <dt> | ||
| 140 | - | <kbd>enter</kbd> | 178 | + | <kbd>?</kbd> |
| 141 | </dt> | 179 | </dt> | ||
| 142 | - | <dd>open row under cursor</dd> | 180 | + | <dd>this list</dd> |
| 143 | </div> | 181 | </div> | ||
| 144 | <div> | 182 | <div> | ||
| 145 | <dt> | 183 | <dt> | ||
| 146 | - | <kbd>g</kbd> | 184 | + | <kbd>j</kbd> |
| 147 | - | <kbd>g</kbd> / <kbd>G</kbd> | 185 | + | <kbd>k</kbd> |
| 148 | </dt> | 186 | </dt> | ||
| 149 | - | <dd>first / last row</dd> | 187 | + | <dd>move the row cursor down / up</dd> |
| 150 | </div> | 188 | </div> | ||
| 151 | <div> | 189 | <div> | ||
| 152 | <dt> | 190 | <dt> | ||
| 153 | - | <kbd>/</kbd> | 191 | + | <kbd>enter</kbd> |
| 154 | </dt> | 192 | </dt> | ||
| 155 | - | <dd>focus the filter</dd> | 193 | + | <dd>open the row under the cursor</dd> |
| 156 | </div> | 194 | </div> | ||
| 157 | <div> | 195 | <div> | ||
| 158 | <dt> | 196 | <dt> | ||
| 159 | - | <kbd>g h</kbd> | 197 | + | <kbd>g g</kbd> |
| 160 | - | <kbd>g t</kbd> | 198 | + | <kbd>G</kbd> |
| 161 | </dt> | 199 | </dt> | ||
| 162 | - | <dd>go home / tree</dd> | 200 | + | <dd>first / last row</dd> |
| 163 | </div> | 201 | </div> | ||
| 164 | <div> | 202 | <div> | ||
| 165 | <dt> | 203 | <dt> | ||
| 166 | - | <kbd>g c</kbd> | 204 | + | <kbd>/</kbd> |
| 167 | - | <kbd>g r</kbd> | |||
| 168 | </dt> | 205 | </dt> | ||
| 169 | - | <dd>go commits / refs</dd> | 206 | + | <dd>filter the list</dd> |
| 170 | </div> | 207 | </div> | ||
| 171 | <div> | 208 | <div> | ||
| 172 | <dt> | 209 | <dt> | ||
| 173 | <kbd>b</kbd> | 210 | <kbd>b</kbd> | ||
| 174 | </dt> | 211 | </dt> | ||
| 175 | - | <dd>switch ref (branches, tags)</dd> | 212 | + | <dd>switch branch or tag</dd> |
| 176 | - | </div> | |||
| 177 | - | <div> | |||
| 178 | - | <dt> | |||
| 179 | - | <kbd>:</kbd> | |||
| 180 | - | <kbd>f</kbd> | |||
| 181 | - | </dt> | |||
| 182 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 183 | - | </div> | |||
| 184 | - | <div> | |||
| 185 | - | <dt> | |||
| 186 | - | <kbd>y</kbd> | |||
| 187 | - | </dt> | |||
| 188 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 189 | </div> | 213 | </div> | ||
| 190 | <div> | 214 | <div> | ||
| 191 | <dt> | 215 | <dt> | ||
| @@ -193,15 +217,21 @@ | |||||
| 193 | <kbd>t</kbd> | 217 | <kbd>t</kbd> | ||
| 194 | <kbd>c</kbd> | 218 | <kbd>c</kbd> | ||
| 195 | <kbd>r</kbd> | 219 | <kbd>r</kbd> | ||
| 220 | + | <kbd>m</kbd> | |||
| 196 | </dt> | 221 | </dt> | ||
| 197 | - | <dd>repo sections (when shown)</dd> | 222 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 198 | </div> | 223 | </div> | ||
| 199 | <div> | 224 | <div> | ||
| 200 | <dt> | 225 | <dt> | ||
| 201 | - | <kbd>m</kbd> | 226 | + | <kbd>g h</kbd> |
| 202 | - | <kbd>g m</kbd> | 227 | + | </dt> |
| 228 | + | <dd>home</dd> | |||
| 229 | + | </div> | |||
| 230 | + | <div> | |||
| 231 | + | <dt> | |||
| 232 | + | <kbd>y</kbd> | |||
| 203 | </dt> | 233 | </dt> | ||
| 204 | - | <dd>merge requests (when shown)</dd> | 234 | + | <dd>copy a permalink pinned to the commit</dd> |
| 205 | </div> | 235 | </div> | ||
| 206 | <div> | 236 | <div> | ||
| 207 | <dt> | 237 | <dt> | ||
| @@ -221,19 +251,21 @@ | |||||
| 221 | <dt> | 251 | <dt> | ||
| 222 | <kbd>esc</kbd> | 252 | <kbd>esc</kbd> | ||
| 223 | </dt> | 253 | </dt> | ||
| 224 | - | <dd>close panel</dd> | 254 | + | <dd>close a dialog, leave a field</dd> |
| 225 | </div> | 255 | </div> | ||
| 226 | </dl> | 256 | </dl> | ||
| 227 | </dialog> | 257 | </dialog> | ||
| 228 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 258 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 229 | <div class="panel-head"> | 259 | <div class="panel-head"> | ||
| 230 | - | <label for="refs-filter">ref></label> | 260 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 231 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 261 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 232 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 262 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 263 | + | <kbd>esc</kbd> | |||
| 264 | + | </button> | |||
| 233 | </div> | 265 | </div> | ||
| 234 | <div class="panel-cols" data-picklist> | 266 | <div class="panel-cols" data-picklist> | ||
| 235 | <section aria-label="Branches"> | 267 | <section aria-label="Branches"> | ||
| 236 | - | <h2>branches <span class="mute">2</span> | 268 | + | <h2>Branches <span class="count">2</span> |
| 237 | </h2> | 269 | </h2> | ||
| 238 | <ul> | 270 | <ul> | ||
| 239 | <li> | 271 | <li> | ||
| @@ -247,7 +279,7 @@ | |||||
| 247 | </ul> | 279 | </ul> | ||
| 248 | </section> | 280 | </section> | ||
| 249 | <section aria-label="Tags"> | 281 | <section aria-label="Tags"> | ||
| 250 | - | <h2>tags <span class="mute">1</span> | 282 | + | <h2>Tags <span class="count">1</span> |
| 251 | </h2> | 283 | </h2> | ||
| 252 | <ul> | 284 | <ul> | ||
| 253 | <li> | 285 | <li> | ||
| @@ -258,105 +290,124 @@ | |||||
| 258 | </section> | 290 | </section> | ||
| 259 | </div> | 291 | </div> | ||
| 260 | </dialog> | 292 | </dialog> | ||
| 261 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 293 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 262 | <div class="panel-head"> | 294 | <div class="panel-head"> | ||
| 263 | - | <label for="cmd-q" class="hi">:</label> | 295 | + | <label for="cmd-q" class="sr">go to</label> |
| 264 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 296 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 265 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 297 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 266 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 298 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 267 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 299 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 268 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 300 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 269 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 301 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 270 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 302 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 271 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 303 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 304 | + | <kbd>esc</kbd> | |||
| 305 | + | </button> | |||
| 272 | </div> | 306 | </div> | ||
| 273 | - | <ul id="cmd-list" class="cmd-list"> | 307 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 274 | <li data-k="go home" data-cursor> | 308 | <li data-k="go home" data-cursor> | ||
| 275 | <a href="/" tabindex="-1"> | 309 | <a href="/" tabindex="-1"> | ||
| 276 | - | <span class="k">go</span>home</a> | 310 | + | <span class="lbl">home</span> |
| 311 | + | <span class="k">go</span> | |||
| 312 | + | </a> | |||
| 277 | </li> | 313 | </li> | ||
| 278 | <li data-k="section summary"> | 314 | <li data-k="section summary"> | ||
| 279 | <a href="/diffs" tabindex="-1"> | 315 | <a href="/diffs" tabindex="-1"> | ||
| 280 | - | <span class="k">section</span>summary</a> | 316 | + | <span class="lbl">summary</span> |
| 317 | + | <span class="k">section</span> | |||
| 318 | + | </a> | |||
| 281 | </li> | 319 | </li> | ||
| 282 | - | <li data-k="section tree"> | 320 | + | <li data-k="section files"> |
| 283 | <a href="/diffs/tree/main" tabindex="-1"> | 321 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 284 | - | <span class="k">section</span>tree</a> | 322 | + | <span class="lbl">files</span> |
| 323 | + | <span class="k">section</span> | |||
| 324 | + | </a> | |||
| 285 | </li> | 325 | </li> | ||
| 286 | <li data-k="section commits"> | 326 | <li data-k="section commits"> | ||
| 287 | <a href="/diffs/commits/main" tabindex="-1"> | 327 | <a href="/diffs/commits/main" tabindex="-1"> | ||
| 288 | - | <span class="k">section</span>commits</a> | 328 | + | <span class="lbl">commits</span> |
| 329 | + | <span class="k">section</span> | |||
| 330 | + | </a> | |||
| 289 | </li> | 331 | </li> | ||
| 290 | - | <li data-k="section refs"> | 332 | + | <li data-k="section branches & tags"> |
| 291 | <a href="/diffs/refs" tabindex="-1"> | 333 | <a href="/diffs/refs" tabindex="-1"> | ||
| 292 | - | <span class="k">section</span>refs</a> | 334 | + | <span class="lbl">branches & tags</span> |
| 335 | + | <span class="k">section</span> | |||
| 336 | + | </a> | |||
| 293 | </li> | 337 | </li> | ||
| 294 | <li data-k="section merge requests"> | 338 | <li data-k="section merge requests"> | ||
| 295 | <a href="/diffs/merge-requests" tabindex="-1"> | 339 | <a href="/diffs/merge-requests" tabindex="-1"> | ||
| 296 | - | <span class="k">section</span>merge requests</a> | 340 | + | <span class="lbl">merge requests</span> |
| 341 | + | <span class="k">section</span> | |||
| 342 | + | </a> | |||
| 297 | </li> | 343 | </li> | ||
| 298 | <li data-k="branch main"> | 344 | <li data-k="branch main"> | ||
| 299 | <a href="/diffs/tree/main" tabindex="-1"> | 345 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 300 | - | <span class="k">branch</span>main</a> | 346 | + | <span class="lbl">main</span> |
| 347 | + | <span class="k">branch</span> | |||
| 348 | + | </a> | |||
| 301 | </li> | 349 | </li> | ||
| 302 | <li data-k="branch side"> | 350 | <li data-k="branch side"> | ||
| 303 | <a href="/diffs/tree/side" tabindex="-1"> | 351 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 304 | - | <span class="k">branch</span>side</a> | 352 | + | <span class="lbl">side</span> |
| 353 | + | <span class="k">branch</span> | |||
| 354 | + | </a> | |||
| 305 | </li> | 355 | </li> | ||
| 306 | <li data-k="tag v1.0.0"> | 356 | <li data-k="tag v1.0.0"> | ||
| 307 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 357 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 308 | - | <span class="k">tag</span>v1.0.0</a> | 358 | + | <span class="lbl">v1.0.0</span> |
| 359 | + | <span class="k">tag</span> | |||
| 360 | + | </a> | |||
| 309 | </li> | 361 | </li> | ||
| 310 | </ul> | 362 | </ul> | ||
| 363 | + | <p class="panel-foot"> | |||
| 364 | + | <span> | |||
| 365 | + | <kbd>↑</kbd> | |||
| 366 | + | <kbd>↓</kbd> move</span> | |||
| 367 | + | <span> | |||
| 368 | + | <kbd>enter</kbd> open</span> | |||
| 369 | + | <span> | |||
| 370 | + | <kbd>esc</kbd> close</span> | |||
| 371 | + | </p> | |||
| 311 | </dialog> | 372 | </dialog> | ||
| 312 | - | <footer class="bar status" aria-label="Status"> | 373 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 313 | - | <span class="mode">commit</span> | |||
| 314 | - | <span class="st-ctx">SHA7 · 1 file · +0 -0</span> | |||
| 315 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 316 | - | <span class="hi">:</span> go to…</button> | |||
| 317 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 318 | - | </span> | |||
| 319 | - | <span class="st-hints">d split · u unified · x collapse all · ? help</span> | |||
| 320 | - | <span class="st-src"> | |||
| 321 | - | <span class="st-ver">ermite devel · </span> | |||
| 322 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 323 | - | </span> | |||
| 324 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 325 | - | </footer> | |||
| 326 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 327 | if (q('dialog[open]').count) return; | 374 | if (q('dialog[open]').count) return; | ||
| 328 | let rows = q('[data-rows] > :not([hidden])').arr(); | 375 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 329 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 376 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 330 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 377 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 331 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 378 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 332 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 379 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 380 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 381 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 333 | })(1); | 382 | })(1); | ||
| 334 | - | k-k from:body -> ((d) => { | 383 | + | k-k from:body -> (async (d) => { |
| 335 | if (q('dialog[open]').count) return; | 384 | if (q('dialog[open]').count) return; | ||
| 336 | let rows = q('[data-rows] > :not([hidden])').arr(); | 385 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 337 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 386 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 338 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 387 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 339 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 388 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 340 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 389 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 390 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 391 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 341 | })(-1); | 392 | })(-1); | ||
| 342 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 393 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 343 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 394 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 344 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 395 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 345 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 396 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 346 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 397 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 347 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 398 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 348 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 399 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 349 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 400 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 350 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 401 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 351 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 402 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 352 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 403 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 353 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 404 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 354 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 405 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 355 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 406 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 356 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 407 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 357 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 408 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 358 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 409 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 359 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 410 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 360 | k-next from:body -> { | 411 | k-next from:body -> { | ||
| 361 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 412 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 362 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 413 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commit-merge.html +164 -113
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>SHA7 merge the side branch · diffs · example.test</title> | 7 | <title>SHA7 merge the side branch · diffs · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,30 +39,49 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">detached</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/diffs" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <header class="show"> | 85 | <header class="show"> | ||
| 63 | <h1>merge the side branch</h1> | 86 | <h1>merge the side branch</h1> | ||
| 64 | <dl class="kv show-kv"> | 87 | <dl class="kv show-kv"> | ||
| @@ -77,9 +100,12 @@ | |||||
| 77 | </span> | 100 | </span> | ||
| 78 | <span class="grow"> | 101 | <span class="grow"> | ||
| 79 | </span> | 102 | </span> | ||
| 80 | - | <a href="/diffs/commit/SHA40?view=unified" class="vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">[u]nified</a> | 103 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">Unified<kbd>u</kbd> |
| 81 | - | <a href="/diffs/commit/SHA40?view=split" class="vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">[d] split</a> | 104 | + | </a> |
| 82 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 105 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">Split<kbd>d</kbd> |
| 106 | + | </a> | |||
| 107 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd> | |||
| 108 | + | </button> | |||
| 83 | </div> | 109 | </div> | ||
| 84 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 110 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | ||
| 85 | <details class="dfile" id="f1" data-file open> | 111 | <details class="dfile" id="f1" data-file open> | ||
| @@ -127,10 +153,8 @@ | |||||
| 127 | </div> | 153 | </div> | ||
| 128 | </main> | 154 | </main> | ||
| 129 | <aside class="rail" aria-label="Context"> | 155 | <aside class="rail" aria-label="Context"> | ||
| 130 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 131 | - | <summary>1 changed file</summary> | |||
| 132 | <section aria-labelledby="r-files"> | 156 | <section aria-labelledby="r-files"> | ||
| 133 | - | <h2 id="r-files">files <span>1</span> | 157 | + | <h2 id="r-files">Files <span class="count">1</span> |
| 134 | </h2> | 158 | </h2> | ||
| 135 | <ol class="files" aria-label="Changed files"> | 159 | <ol class="files" aria-label="Changed files"> | ||
| 136 | <li> | 160 | <li> | ||
| @@ -150,73 +174,73 @@ | |||||
| 150 | </li> | 174 | </li> | ||
| 151 | </ol> | 175 | </ol> | ||
| 152 | </section> | 176 | </section> | ||
| 153 | - | </details> | |||
| 154 | </aside> | 177 | </aside> | ||
| 155 | </div> | 178 | </div> | ||
| 156 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 179 | + | <footer class="foot"> |
| 180 | + | <span>ermite devel</span> | |||
| 181 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 182 | + | <span class="foot-keys"> | |||
| 183 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 184 | + | </footer> | |||
| 185 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 186 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 187 | + | </span> | |||
| 188 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 189 | + | </span> | |||
| 190 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 191 | + | </span> | |||
| 192 | + | </div> | |||
| 193 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 157 | <div class="panel-head"> | 194 | <div class="panel-head"> | ||
| 158 | - | <span>keys</span> | 195 | + | <h2>Keyboard shortcuts</h2> |
| 159 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 196 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 197 | + | <kbd>esc</kbd> close</button> | |||
| 160 | </div> | 198 | </div> | ||
| 161 | <dl class="keys"> | 199 | <dl class="keys"> | ||
| 162 | <div> | 200 | <div> | ||
| 163 | <dt> | 201 | <dt> | ||
| 164 | - | <kbd>j</kbd> | 202 | + | <kbd>ctrl k</kbd> |
| 165 | - | <kbd>k</kbd> | 203 | + | <kbd>:</kbd> |
| 166 | </dt> | 204 | </dt> | ||
| 167 | - | <dd>move cursor down / up</dd> | 205 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 168 | </div> | 206 | </div> | ||
| 169 | <div> | 207 | <div> | ||
| 170 | <dt> | 208 | <dt> | ||
| 171 | - | <kbd>enter</kbd> | 209 | + | <kbd>?</kbd> |
| 172 | </dt> | 210 | </dt> | ||
| 173 | - | <dd>open row under cursor</dd> | 211 | + | <dd>this list</dd> |
| 174 | </div> | 212 | </div> | ||
| 175 | <div> | 213 | <div> | ||
| 176 | <dt> | 214 | <dt> | ||
| 177 | - | <kbd>g</kbd> | 215 | + | <kbd>j</kbd> |
| 178 | - | <kbd>g</kbd> / <kbd>G</kbd> | 216 | + | <kbd>k</kbd> |
| 179 | </dt> | 217 | </dt> | ||
| 180 | - | <dd>first / last row</dd> | 218 | + | <dd>move the row cursor down / up</dd> |
| 181 | </div> | 219 | </div> | ||
| 182 | <div> | 220 | <div> | ||
| 183 | <dt> | 221 | <dt> | ||
| 184 | - | <kbd>/</kbd> | 222 | + | <kbd>enter</kbd> |
| 185 | </dt> | 223 | </dt> | ||
| 186 | - | <dd>focus the filter</dd> | 224 | + | <dd>open the row under the cursor</dd> |
| 187 | </div> | 225 | </div> | ||
| 188 | <div> | 226 | <div> | ||
| 189 | <dt> | 227 | <dt> | ||
| 190 | - | <kbd>g h</kbd> | 228 | + | <kbd>g g</kbd> |
| 191 | - | <kbd>g t</kbd> | 229 | + | <kbd>G</kbd> |
| 192 | </dt> | 230 | </dt> | ||
| 193 | - | <dd>go home / tree</dd> | 231 | + | <dd>first / last row</dd> |
| 194 | </div> | 232 | </div> | ||
| 195 | <div> | 233 | <div> | ||
| 196 | <dt> | 234 | <dt> | ||
| 197 | - | <kbd>g c</kbd> | 235 | + | <kbd>/</kbd> |
| 198 | - | <kbd>g r</kbd> | |||
| 199 | </dt> | 236 | </dt> | ||
| 200 | - | <dd>go commits / refs</dd> | 237 | + | <dd>filter the list</dd> |
| 201 | </div> | 238 | </div> | ||
| 202 | <div> | 239 | <div> | ||
| 203 | <dt> | 240 | <dt> | ||
| 204 | <kbd>b</kbd> | 241 | <kbd>b</kbd> | ||
| 205 | </dt> | 242 | </dt> | ||
| 206 | - | <dd>switch ref (branches, tags)</dd> | 243 | + | <dd>switch branch or tag</dd> |
| 207 | - | </div> | |||
| 208 | - | <div> | |||
| 209 | - | <dt> | |||
| 210 | - | <kbd>:</kbd> | |||
| 211 | - | <kbd>f</kbd> | |||
| 212 | - | </dt> | |||
| 213 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 214 | - | </div> | |||
| 215 | - | <div> | |||
| 216 | - | <dt> | |||
| 217 | - | <kbd>y</kbd> | |||
| 218 | - | </dt> | |||
| 219 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 220 | </div> | 244 | </div> | ||
| 221 | <div> | 245 | <div> | ||
| 222 | <dt> | 246 | <dt> | ||
| @@ -224,15 +248,21 @@ | |||||
| 224 | <kbd>t</kbd> | 248 | <kbd>t</kbd> | ||
| 225 | <kbd>c</kbd> | 249 | <kbd>c</kbd> | ||
| 226 | <kbd>r</kbd> | 250 | <kbd>r</kbd> | ||
| 251 | + | <kbd>m</kbd> | |||
| 227 | </dt> | 252 | </dt> | ||
| 228 | - | <dd>repo sections (when shown)</dd> | 253 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 229 | </div> | 254 | </div> | ||
| 230 | <div> | 255 | <div> | ||
| 231 | <dt> | 256 | <dt> | ||
| 232 | - | <kbd>m</kbd> | 257 | + | <kbd>g h</kbd> |
| 233 | - | <kbd>g m</kbd> | 258 | + | </dt> |
| 259 | + | <dd>home</dd> | |||
| 260 | + | </div> | |||
| 261 | + | <div> | |||
| 262 | + | <dt> | |||
| 263 | + | <kbd>y</kbd> | |||
| 234 | </dt> | 264 | </dt> | ||
| 235 | - | <dd>merge requests (when shown)</dd> | 265 | + | <dd>copy a permalink pinned to the commit</dd> |
| 236 | </div> | 266 | </div> | ||
| 237 | <div> | 267 | <div> | ||
| 238 | <dt> | 268 | <dt> | ||
| @@ -252,19 +282,21 @@ | |||||
| 252 | <dt> | 282 | <dt> | ||
| 253 | <kbd>esc</kbd> | 283 | <kbd>esc</kbd> | ||
| 254 | </dt> | 284 | </dt> | ||
| 255 | - | <dd>close panel</dd> | 285 | + | <dd>close a dialog, leave a field</dd> |
| 256 | </div> | 286 | </div> | ||
| 257 | </dl> | 287 | </dl> | ||
| 258 | </dialog> | 288 | </dialog> | ||
| 259 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 289 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 260 | <div class="panel-head"> | 290 | <div class="panel-head"> | ||
| 261 | - | <label for="refs-filter">ref></label> | 291 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 262 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 292 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 263 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 293 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 294 | + | <kbd>esc</kbd> | |||
| 295 | + | </button> | |||
| 264 | </div> | 296 | </div> | ||
| 265 | <div class="panel-cols" data-picklist> | 297 | <div class="panel-cols" data-picklist> | ||
| 266 | <section aria-label="Branches"> | 298 | <section aria-label="Branches"> | ||
| 267 | - | <h2>branches <span class="mute">2</span> | 299 | + | <h2>Branches <span class="count">2</span> |
| 268 | </h2> | 300 | </h2> | ||
| 269 | <ul> | 301 | <ul> | ||
| 270 | <li> | 302 | <li> | ||
| @@ -278,7 +310,7 @@ | |||||
| 278 | </ul> | 310 | </ul> | ||
| 279 | </section> | 311 | </section> | ||
| 280 | <section aria-label="Tags"> | 312 | <section aria-label="Tags"> | ||
| 281 | - | <h2>tags <span class="mute">1</span> | 313 | + | <h2>Tags <span class="count">1</span> |
| 282 | </h2> | 314 | </h2> | ||
| 283 | <ul> | 315 | <ul> | ||
| 284 | <li> | 316 | <li> | ||
| @@ -289,105 +321,124 @@ | |||||
| 289 | </section> | 321 | </section> | ||
| 290 | </div> | 322 | </div> | ||
| 291 | </dialog> | 323 | </dialog> | ||
| 292 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 324 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 293 | <div class="panel-head"> | 325 | <div class="panel-head"> | ||
| 294 | - | <label for="cmd-q" class="hi">:</label> | 326 | + | <label for="cmd-q" class="sr">go to</label> |
| 295 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 327 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 296 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 328 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 297 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 329 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 298 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 330 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 299 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 331 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 300 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 332 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 301 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 333 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 302 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 334 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 335 | + | <kbd>esc</kbd> | |||
| 336 | + | </button> | |||
| 303 | </div> | 337 | </div> | ||
| 304 | - | <ul id="cmd-list" class="cmd-list"> | 338 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 305 | <li data-k="go home" data-cursor> | 339 | <li data-k="go home" data-cursor> | ||
| 306 | <a href="/" tabindex="-1"> | 340 | <a href="/" tabindex="-1"> | ||
| 307 | - | <span class="k">go</span>home</a> | 341 | + | <span class="lbl">home</span> |
| 342 | + | <span class="k">go</span> | |||
| 343 | + | </a> | |||
| 308 | </li> | 344 | </li> | ||
| 309 | <li data-k="section summary"> | 345 | <li data-k="section summary"> | ||
| 310 | <a href="/diffs" tabindex="-1"> | 346 | <a href="/diffs" tabindex="-1"> | ||
| 311 | - | <span class="k">section</span>summary</a> | 347 | + | <span class="lbl">summary</span> |
| 348 | + | <span class="k">section</span> | |||
| 349 | + | </a> | |||
| 312 | </li> | 350 | </li> | ||
| 313 | - | <li data-k="section tree"> | 351 | + | <li data-k="section files"> |
| 314 | <a href="/diffs/tree/main" tabindex="-1"> | 352 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 315 | - | <span class="k">section</span>tree</a> | 353 | + | <span class="lbl">files</span> |
| 354 | + | <span class="k">section</span> | |||
| 355 | + | </a> | |||
| 316 | </li> | 356 | </li> | ||
| 317 | <li data-k="section commits"> | 357 | <li data-k="section commits"> | ||
| 318 | <a href="/diffs/commits/main" tabindex="-1"> | 358 | <a href="/diffs/commits/main" tabindex="-1"> | ||
| 319 | - | <span class="k">section</span>commits</a> | 359 | + | <span class="lbl">commits</span> |
| 360 | + | <span class="k">section</span> | |||
| 361 | + | </a> | |||
| 320 | </li> | 362 | </li> | ||
| 321 | - | <li data-k="section refs"> | 363 | + | <li data-k="section branches & tags"> |
| 322 | <a href="/diffs/refs" tabindex="-1"> | 364 | <a href="/diffs/refs" tabindex="-1"> | ||
| 323 | - | <span class="k">section</span>refs</a> | 365 | + | <span class="lbl">branches & tags</span> |
| 366 | + | <span class="k">section</span> | |||
| 367 | + | </a> | |||
| 324 | </li> | 368 | </li> | ||
| 325 | <li data-k="section merge requests"> | 369 | <li data-k="section merge requests"> | ||
| 326 | <a href="/diffs/merge-requests" tabindex="-1"> | 370 | <a href="/diffs/merge-requests" tabindex="-1"> | ||
| 327 | - | <span class="k">section</span>merge requests</a> | 371 | + | <span class="lbl">merge requests</span> |
| 372 | + | <span class="k">section</span> | |||
| 373 | + | </a> | |||
| 328 | </li> | 374 | </li> | ||
| 329 | <li data-k="branch main"> | 375 | <li data-k="branch main"> | ||
| 330 | <a href="/diffs/tree/main" tabindex="-1"> | 376 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 331 | - | <span class="k">branch</span>main</a> | 377 | + | <span class="lbl">main</span> |
| 378 | + | <span class="k">branch</span> | |||
| 379 | + | </a> | |||
| 332 | </li> | 380 | </li> | ||
| 333 | <li data-k="branch side"> | 381 | <li data-k="branch side"> | ||
| 334 | <a href="/diffs/tree/side" tabindex="-1"> | 382 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 335 | - | <span class="k">branch</span>side</a> | 383 | + | <span class="lbl">side</span> |
| 384 | + | <span class="k">branch</span> | |||
| 385 | + | </a> | |||
| 336 | </li> | 386 | </li> | ||
| 337 | <li data-k="tag v1.0.0"> | 387 | <li data-k="tag v1.0.0"> | ||
| 338 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 388 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 339 | - | <span class="k">tag</span>v1.0.0</a> | 389 | + | <span class="lbl">v1.0.0</span> |
| 390 | + | <span class="k">tag</span> | |||
| 391 | + | </a> | |||
| 340 | </li> | 392 | </li> | ||
| 341 | </ul> | 393 | </ul> | ||
| 394 | + | <p class="panel-foot"> | |||
| 395 | + | <span> | |||
| 396 | + | <kbd>↑</kbd> | |||
| 397 | + | <kbd>↓</kbd> move</span> | |||
| 398 | + | <span> | |||
| 399 | + | <kbd>enter</kbd> open</span> | |||
| 400 | + | <span> | |||
| 401 | + | <kbd>esc</kbd> close</span> | |||
| 402 | + | </p> | |||
| 342 | </dialog> | 403 | </dialog> | ||
| 343 | - | <footer class="bar status" aria-label="Status"> | 404 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 344 | - | <span class="mode">commit</span> | |||
| 345 | - | <span class="st-ctx">SHA7 · 1 file · +1 -0</span> | |||
| 346 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 347 | - | <span class="hi">:</span> go to…</button> | |||
| 348 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 349 | - | </span> | |||
| 350 | - | <span class="st-hints">d split · u unified · x collapse all · ? help</span> | |||
| 351 | - | <span class="st-src"> | |||
| 352 | - | <span class="st-ver">ermite devel · </span> | |||
| 353 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 354 | - | </span> | |||
| 355 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 356 | - | </footer> | |||
| 357 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 358 | if (q('dialog[open]').count) return; | 405 | if (q('dialog[open]').count) return; | ||
| 359 | let rows = q('[data-rows] > :not([hidden])').arr(); | 406 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 360 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 407 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 361 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 408 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 362 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 409 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 363 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 410 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 411 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 412 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 364 | })(1); | 413 | })(1); | ||
| 365 | - | k-k from:body -> ((d) => { | 414 | + | k-k from:body -> (async (d) => { |
| 366 | if (q('dialog[open]').count) return; | 415 | if (q('dialog[open]').count) return; | ||
| 367 | let rows = q('[data-rows] > :not([hidden])').arr(); | 416 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 368 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 417 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 369 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 418 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 370 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 419 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 371 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 420 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 421 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 422 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 372 | })(-1); | 423 | })(-1); | ||
| 373 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 424 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 374 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 425 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 375 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 426 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 376 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 427 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 377 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 428 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 378 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 429 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 379 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 430 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 380 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 431 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 381 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 432 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 382 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 433 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 383 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 434 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 384 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 435 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 385 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 436 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 386 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 437 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 387 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 438 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 388 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 439 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 389 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 440 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 390 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 441 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 391 | k-next from:body -> { | 442 | k-next from:body -> { | ||
| 392 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 443 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 393 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 444 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commit-modify-split.html +167 -114
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>SHA7 modify a file and make a script executable · diffs · example.test</title> | 7 | <title>SHA7 modify a file and make a script executable · diffs · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,30 +39,49 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">detached</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/diffs" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <header class="show"> | 85 | <header class="show"> | ||
| 63 | <h1>modify a file and make a script executable</h1> | 86 | <h1>modify a file and make a script executable</h1> | ||
| 64 | <pre class="msg">The body has a second paragraph. | 87 | <pre class="msg">The body has a second paragraph. | ||
| @@ -73,7 +96,9 @@ And <b>html</b> & "quotes".</pre> | |||||
| 73 | <a href="/diffs/commit/SHA40">SHA7</a> | 96 | <a href="/diffs/commit/SHA40">SHA7</a> | ||
| 74 | </dd> | 97 | </dd> | ||
| 75 | <dt>refs</dt> | 98 | <dt>refs</dt> | ||
| 76 | - | <dd>[tag: v1.0.0] </dd> | 99 | + | <dd> |
| 100 | + | <span class="pill">tag: v1.0.0</span> | |||
| 101 | + | </dd> | |||
| 77 | </dl> | 102 | </dl> | ||
| 78 | </header> | 103 | </header> | ||
| 79 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | 104 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | ||
| @@ -82,9 +107,12 @@ And <b>html</b> & "quotes".</pre> | |||||
| 82 | </span> | 107 | </span> | ||
| 83 | <span class="grow"> | 108 | <span class="grow"> | ||
| 84 | </span> | 109 | </span> | ||
| 85 | - | <a href="/diffs/commit/SHA40?view=unified" class="vbtn" data-view="unified" aria-keyshortcuts="u" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">[u]nified</a> | 110 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">Unified<kbd>u</kbd> |
| 86 | - | <a href="/diffs/commit/SHA40?view=split" class="vbtn" data-view="split" aria-keyshortcuts="d" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">[d] split</a> | 111 | + | </a> |
| 87 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 112 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">Split<kbd>d</kbd> |
| 113 | + | </a> | |||
| 114 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd> | |||
| 115 | + | </button> | |||
| 88 | </div> | 116 | </div> | ||
| 89 | <div id="diffs" data-diff="split"> | 117 | <div id="diffs" data-diff="split"> | ||
| 90 | <details class="dfile" id="f1" data-file open> | 118 | <details class="dfile" id="f1" data-file open> | ||
| @@ -232,10 +260,8 @@ And <b>html</b> & "quotes".</pre> | |||||
| 232 | </div> | 260 | </div> | ||
| 233 | </main> | 261 | </main> | ||
| 234 | <aside class="rail" aria-label="Context"> | 262 | <aside class="rail" aria-label="Context"> | ||
| 235 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 236 | - | <summary>2 changed files</summary> | |||
| 237 | <section aria-labelledby="r-files"> | 263 | <section aria-labelledby="r-files"> | ||
| 238 | - | <h2 id="r-files">files <span>2</span> | 264 | + | <h2 id="r-files">Files <span class="count">2</span> |
| 239 | </h2> | 265 | </h2> | ||
| 240 | <ol class="files" aria-label="Changed files"> | 266 | <ol class="files" aria-label="Changed files"> | ||
| 241 | <li> | 267 | <li> | ||
| @@ -269,73 +295,73 @@ And <b>html</b> & "quotes".</pre> | |||||
| 269 | </li> | 295 | </li> | ||
| 270 | </ol> | 296 | </ol> | ||
| 271 | </section> | 297 | </section> | ||
| 272 | - | </details> | |||
| 273 | </aside> | 298 | </aside> | ||
| 274 | </div> | 299 | </div> | ||
| 275 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 300 | + | <footer class="foot"> |
| 301 | + | <span>ermite devel</span> | |||
| 302 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 303 | + | <span class="foot-keys"> | |||
| 304 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 305 | + | </footer> | |||
| 306 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 307 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 308 | + | </span> | |||
| 309 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 310 | + | </span> | |||
| 311 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 312 | + | </span> | |||
| 313 | + | </div> | |||
| 314 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 276 | <div class="panel-head"> | 315 | <div class="panel-head"> | ||
| 277 | - | <span>keys</span> | 316 | + | <h2>Keyboard shortcuts</h2> |
| 278 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 317 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 318 | + | <kbd>esc</kbd> close</button> | |||
| 279 | </div> | 319 | </div> | ||
| 280 | <dl class="keys"> | 320 | <dl class="keys"> | ||
| 281 | <div> | 321 | <div> | ||
| 282 | <dt> | 322 | <dt> | ||
| 283 | - | <kbd>j</kbd> | 323 | + | <kbd>ctrl k</kbd> |
| 284 | - | <kbd>k</kbd> | 324 | + | <kbd>:</kbd> |
| 285 | </dt> | 325 | </dt> | ||
| 286 | - | <dd>move cursor down / up</dd> | 326 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 287 | </div> | 327 | </div> | ||
| 288 | <div> | 328 | <div> | ||
| 289 | <dt> | 329 | <dt> | ||
| 290 | - | <kbd>enter</kbd> | 330 | + | <kbd>?</kbd> |
| 291 | </dt> | 331 | </dt> | ||
| 292 | - | <dd>open row under cursor</dd> | 332 | + | <dd>this list</dd> |
| 293 | </div> | 333 | </div> | ||
| 294 | <div> | 334 | <div> | ||
| 295 | <dt> | 335 | <dt> | ||
| 296 | - | <kbd>g</kbd> | 336 | + | <kbd>j</kbd> |
| 297 | - | <kbd>g</kbd> / <kbd>G</kbd> | 337 | + | <kbd>k</kbd> |
| 298 | </dt> | 338 | </dt> | ||
| 299 | - | <dd>first / last row</dd> | 339 | + | <dd>move the row cursor down / up</dd> |
| 300 | </div> | 340 | </div> | ||
| 301 | <div> | 341 | <div> | ||
| 302 | <dt> | 342 | <dt> | ||
| 303 | - | <kbd>/</kbd> | 343 | + | <kbd>enter</kbd> |
| 304 | </dt> | 344 | </dt> | ||
| 305 | - | <dd>focus the filter</dd> | 345 | + | <dd>open the row under the cursor</dd> |
| 306 | </div> | 346 | </div> | ||
| 307 | <div> | 347 | <div> | ||
| 308 | <dt> | 348 | <dt> | ||
| 309 | - | <kbd>g h</kbd> | 349 | + | <kbd>g g</kbd> |
| 310 | - | <kbd>g t</kbd> | 350 | + | <kbd>G</kbd> |
| 311 | </dt> | 351 | </dt> | ||
| 312 | - | <dd>go home / tree</dd> | 352 | + | <dd>first / last row</dd> |
| 313 | </div> | 353 | </div> | ||
| 314 | <div> | 354 | <div> | ||
| 315 | <dt> | 355 | <dt> | ||
| 316 | - | <kbd>g c</kbd> | 356 | + | <kbd>/</kbd> |
| 317 | - | <kbd>g r</kbd> | |||
| 318 | </dt> | 357 | </dt> | ||
| 319 | - | <dd>go commits / refs</dd> | 358 | + | <dd>filter the list</dd> |
| 320 | </div> | 359 | </div> | ||
| 321 | <div> | 360 | <div> | ||
| 322 | <dt> | 361 | <dt> | ||
| 323 | <kbd>b</kbd> | 362 | <kbd>b</kbd> | ||
| 324 | </dt> | 363 | </dt> | ||
| 325 | - | <dd>switch ref (branches, tags)</dd> | 364 | + | <dd>switch branch or tag</dd> |
| 326 | - | </div> | |||
| 327 | - | <div> | |||
| 328 | - | <dt> | |||
| 329 | - | <kbd>:</kbd> | |||
| 330 | - | <kbd>f</kbd> | |||
| 331 | - | </dt> | |||
| 332 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 333 | - | </div> | |||
| 334 | - | <div> | |||
| 335 | - | <dt> | |||
| 336 | - | <kbd>y</kbd> | |||
| 337 | - | </dt> | |||
| 338 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 339 | </div> | 365 | </div> | ||
| 340 | <div> | 366 | <div> | ||
| 341 | <dt> | 367 | <dt> | ||
| @@ -343,15 +369,21 @@ And <b>html</b> & "quotes".</pre> | |||||
| 343 | <kbd>t</kbd> | 369 | <kbd>t</kbd> | ||
| 344 | <kbd>c</kbd> | 370 | <kbd>c</kbd> | ||
| 345 | <kbd>r</kbd> | 371 | <kbd>r</kbd> | ||
| 372 | + | <kbd>m</kbd> | |||
| 346 | </dt> | 373 | </dt> | ||
| 347 | - | <dd>repo sections (when shown)</dd> | 374 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 348 | </div> | 375 | </div> | ||
| 349 | <div> | 376 | <div> | ||
| 350 | <dt> | 377 | <dt> | ||
| 351 | - | <kbd>m</kbd> | 378 | + | <kbd>g h</kbd> |
| 352 | - | <kbd>g m</kbd> | 379 | + | </dt> |
| 380 | + | <dd>home</dd> | |||
| 381 | + | </div> | |||
| 382 | + | <div> | |||
| 383 | + | <dt> | |||
| 384 | + | <kbd>y</kbd> | |||
| 353 | </dt> | 385 | </dt> | ||
| 354 | - | <dd>merge requests (when shown)</dd> | 386 | + | <dd>copy a permalink pinned to the commit</dd> |
| 355 | </div> | 387 | </div> | ||
| 356 | <div> | 388 | <div> | ||
| 357 | <dt> | 389 | <dt> | ||
| @@ -371,19 +403,21 @@ And <b>html</b> & "quotes".</pre> | |||||
| 371 | <dt> | 403 | <dt> | ||
| 372 | <kbd>esc</kbd> | 404 | <kbd>esc</kbd> | ||
| 373 | </dt> | 405 | </dt> | ||
| 374 | - | <dd>close panel</dd> | 406 | + | <dd>close a dialog, leave a field</dd> |
| 375 | </div> | 407 | </div> | ||
| 376 | </dl> | 408 | </dl> | ||
| 377 | </dialog> | 409 | </dialog> | ||
| 378 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 410 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 379 | <div class="panel-head"> | 411 | <div class="panel-head"> | ||
| 380 | - | <label for="refs-filter">ref></label> | 412 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 381 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 413 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 382 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 414 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 415 | + | <kbd>esc</kbd> | |||
| 416 | + | </button> | |||
| 383 | </div> | 417 | </div> | ||
| 384 | <div class="panel-cols" data-picklist> | 418 | <div class="panel-cols" data-picklist> | ||
| 385 | <section aria-label="Branches"> | 419 | <section aria-label="Branches"> | ||
| 386 | - | <h2>branches <span class="mute">2</span> | 420 | + | <h2>Branches <span class="count">2</span> |
| 387 | </h2> | 421 | </h2> | ||
| 388 | <ul> | 422 | <ul> | ||
| 389 | <li> | 423 | <li> | ||
| @@ -397,7 +431,7 @@ And <b>html</b> & "quotes".</pre> | |||||
| 397 | </ul> | 431 | </ul> | ||
| 398 | </section> | 432 | </section> | ||
| 399 | <section aria-label="Tags"> | 433 | <section aria-label="Tags"> | ||
| 400 | - | <h2>tags <span class="mute">1</span> | 434 | + | <h2>Tags <span class="count">1</span> |
| 401 | </h2> | 435 | </h2> | ||
| 402 | <ul> | 436 | <ul> | ||
| 403 | <li> | 437 | <li> | ||
| @@ -408,105 +442,124 @@ And <b>html</b> & "quotes".</pre> | |||||
| 408 | </section> | 442 | </section> | ||
| 409 | </div> | 443 | </div> | ||
| 410 | </dialog> | 444 | </dialog> | ||
| 411 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 445 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 412 | <div class="panel-head"> | 446 | <div class="panel-head"> | ||
| 413 | - | <label for="cmd-q" class="hi">:</label> | 447 | + | <label for="cmd-q" class="sr">go to</label> |
| 414 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 448 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 415 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 449 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 416 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 450 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 417 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 451 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 418 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 452 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 419 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 453 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 420 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 454 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 421 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 455 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 456 | + | <kbd>esc</kbd> | |||
| 457 | + | </button> | |||
| 422 | </div> | 458 | </div> | ||
| 423 | - | <ul id="cmd-list" class="cmd-list"> | 459 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 424 | <li data-k="go home" data-cursor> | 460 | <li data-k="go home" data-cursor> | ||
| 425 | <a href="/" tabindex="-1"> | 461 | <a href="/" tabindex="-1"> | ||
| 426 | - | <span class="k">go</span>home</a> | 462 | + | <span class="lbl">home</span> |
| 463 | + | <span class="k">go</span> | |||
| 464 | + | </a> | |||
| 427 | </li> | 465 | </li> | ||
| 428 | <li data-k="section summary"> | 466 | <li data-k="section summary"> | ||
| 429 | <a href="/diffs" tabindex="-1"> | 467 | <a href="/diffs" tabindex="-1"> | ||
| 430 | - | <span class="k">section</span>summary</a> | 468 | + | <span class="lbl">summary</span> |
| 469 | + | <span class="k">section</span> | |||
| 470 | + | </a> | |||
| 431 | </li> | 471 | </li> | ||
| 432 | - | <li data-k="section tree"> | 472 | + | <li data-k="section files"> |
| 433 | <a href="/diffs/tree/main" tabindex="-1"> | 473 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 434 | - | <span class="k">section</span>tree</a> | 474 | + | <span class="lbl">files</span> |
| 475 | + | <span class="k">section</span> | |||
| 476 | + | </a> | |||
| 435 | </li> | 477 | </li> | ||
| 436 | <li data-k="section commits"> | 478 | <li data-k="section commits"> | ||
| 437 | <a href="/diffs/commits/main" tabindex="-1"> | 479 | <a href="/diffs/commits/main" tabindex="-1"> | ||
| 438 | - | <span class="k">section</span>commits</a> | 480 | + | <span class="lbl">commits</span> |
| 481 | + | <span class="k">section</span> | |||
| 482 | + | </a> | |||
| 439 | </li> | 483 | </li> | ||
| 440 | - | <li data-k="section refs"> | 484 | + | <li data-k="section branches & tags"> |
| 441 | <a href="/diffs/refs" tabindex="-1"> | 485 | <a href="/diffs/refs" tabindex="-1"> | ||
| 442 | - | <span class="k">section</span>refs</a> | 486 | + | <span class="lbl">branches & tags</span> |
| 487 | + | <span class="k">section</span> | |||
| 488 | + | </a> | |||
| 443 | </li> | 489 | </li> | ||
| 444 | <li data-k="section merge requests"> | 490 | <li data-k="section merge requests"> | ||
| 445 | <a href="/diffs/merge-requests" tabindex="-1"> | 491 | <a href="/diffs/merge-requests" tabindex="-1"> | ||
| 446 | - | <span class="k">section</span>merge requests</a> | 492 | + | <span class="lbl">merge requests</span> |
| 493 | + | <span class="k">section</span> | |||
| 494 | + | </a> | |||
| 447 | </li> | 495 | </li> | ||
| 448 | <li data-k="branch main"> | 496 | <li data-k="branch main"> | ||
| 449 | <a href="/diffs/tree/main" tabindex="-1"> | 497 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 450 | - | <span class="k">branch</span>main</a> | 498 | + | <span class="lbl">main</span> |
| 499 | + | <span class="k">branch</span> | |||
| 500 | + | </a> | |||
| 451 | </li> | 501 | </li> | ||
| 452 | <li data-k="branch side"> | 502 | <li data-k="branch side"> | ||
| 453 | <a href="/diffs/tree/side" tabindex="-1"> | 503 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 454 | - | <span class="k">branch</span>side</a> | 504 | + | <span class="lbl">side</span> |
| 505 | + | <span class="k">branch</span> | |||
| 506 | + | </a> | |||
| 455 | </li> | 507 | </li> | ||
| 456 | <li data-k="tag v1.0.0"> | 508 | <li data-k="tag v1.0.0"> | ||
| 457 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 509 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 458 | - | <span class="k">tag</span>v1.0.0</a> | 510 | + | <span class="lbl">v1.0.0</span> |
| 511 | + | <span class="k">tag</span> | |||
| 512 | + | </a> | |||
| 459 | </li> | 513 | </li> | ||
| 460 | </ul> | 514 | </ul> | ||
| 515 | + | <p class="panel-foot"> | |||
| 516 | + | <span> | |||
| 517 | + | <kbd>↑</kbd> | |||
| 518 | + | <kbd>↓</kbd> move</span> | |||
| 519 | + | <span> | |||
| 520 | + | <kbd>enter</kbd> open</span> | |||
| 521 | + | <span> | |||
| 522 | + | <kbd>esc</kbd> close</span> | |||
| 523 | + | </p> | |||
| 461 | </dialog> | 524 | </dialog> | ||
| 462 | - | <footer class="bar status" aria-label="Status"> | 525 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 463 | - | <span class="mode">commit</span> | |||
| 464 | - | <span class="st-ctx">SHA7 · 2 files · +2 -1</span> | |||
| 465 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 466 | - | <span class="hi">:</span> go to…</button> | |||
| 467 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 468 | - | </span> | |||
| 469 | - | <span class="st-hints">d split · u unified · x collapse all · ? help</span> | |||
| 470 | - | <span class="st-src"> | |||
| 471 | - | <span class="st-ver">ermite devel · </span> | |||
| 472 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 473 | - | </span> | |||
| 474 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 475 | - | </footer> | |||
| 476 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 477 | if (q('dialog[open]').count) return; | 526 | if (q('dialog[open]').count) return; | ||
| 478 | let rows = q('[data-rows] > :not([hidden])').arr(); | 527 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 479 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 528 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 480 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 529 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 481 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 530 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 482 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 531 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 532 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 533 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 483 | })(1); | 534 | })(1); | ||
| 484 | - | k-k from:body -> ((d) => { | 535 | + | k-k from:body -> (async (d) => { |
| 485 | if (q('dialog[open]').count) return; | 536 | if (q('dialog[open]').count) return; | ||
| 486 | let rows = q('[data-rows] > :not([hidden])').arr(); | 537 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 487 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 538 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 488 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 539 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 489 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 540 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 490 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 541 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 542 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 543 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 491 | })(-1); | 544 | })(-1); | ||
| 492 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 545 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 493 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 546 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 494 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 547 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 495 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 548 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 496 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 549 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 497 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 550 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 498 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 551 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 499 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 552 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 500 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 553 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 501 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 554 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 502 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 555 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 503 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 556 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 504 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 557 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 505 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 558 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 506 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 559 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 507 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 560 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 508 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 561 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 509 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 562 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 510 | k-next from:body -> { | 563 | k-next from:body -> { | ||
| 511 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 564 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 512 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 565 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commit-modify.html +167 -114
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>SHA7 modify a file and make a script executable · diffs · example.test</title> | 7 | <title>SHA7 modify a file and make a script executable · diffs · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,30 +39,49 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">detached</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/diffs" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <header class="show"> | 85 | <header class="show"> | ||
| 63 | <h1>modify a file and make a script executable</h1> | 86 | <h1>modify a file and make a script executable</h1> | ||
| 64 | <pre class="msg">The body has a second paragraph. | 87 | <pre class="msg">The body has a second paragraph. | ||
| @@ -73,7 +96,9 @@ And <b>html</b> & "quotes".</pre> | |||||
| 73 | <a href="/diffs/commit/SHA40">SHA7</a> | 96 | <a href="/diffs/commit/SHA40">SHA7</a> | ||
| 74 | </dd> | 97 | </dd> | ||
| 75 | <dt>refs</dt> | 98 | <dt>refs</dt> | ||
| 76 | - | <dd>[tag: v1.0.0] </dd> | 99 | + | <dd> |
| 100 | + | <span class="pill">tag: v1.0.0</span> | |||
| 101 | + | </dd> | |||
| 77 | </dl> | 102 | </dl> | ||
| 78 | </header> | 103 | </header> | ||
| 79 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | 104 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | ||
| @@ -82,9 +107,12 @@ And <b>html</b> & "quotes".</pre> | |||||
| 82 | </span> | 107 | </span> | ||
| 83 | <span class="grow"> | 108 | <span class="grow"> | ||
| 84 | </span> | 109 | </span> | ||
| 85 | - | <a href="/diffs/commit/SHA40?view=unified" class="vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">[u]nified</a> | 110 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">Unified<kbd>u</kbd> |
| 86 | - | <a href="/diffs/commit/SHA40?view=split" class="vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">[d] split</a> | 111 | + | </a> |
| 87 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 112 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">Split<kbd>d</kbd> |
| 113 | + | </a> | |||
| 114 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd> | |||
| 115 | + | </button> | |||
| 88 | </div> | 116 | </div> | ||
| 89 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 117 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | ||
| 90 | <details class="dfile" id="f1" data-file open> | 118 | <details class="dfile" id="f1" data-file open> | ||
| @@ -232,10 +260,8 @@ And <b>html</b> & "quotes".</pre> | |||||
| 232 | </div> | 260 | </div> | ||
| 233 | </main> | 261 | </main> | ||
| 234 | <aside class="rail" aria-label="Context"> | 262 | <aside class="rail" aria-label="Context"> | ||
| 235 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 236 | - | <summary>2 changed files</summary> | |||
| 237 | <section aria-labelledby="r-files"> | 263 | <section aria-labelledby="r-files"> | ||
| 238 | - | <h2 id="r-files">files <span>2</span> | 264 | + | <h2 id="r-files">Files <span class="count">2</span> |
| 239 | </h2> | 265 | </h2> | ||
| 240 | <ol class="files" aria-label="Changed files"> | 266 | <ol class="files" aria-label="Changed files"> | ||
| 241 | <li> | 267 | <li> | ||
| @@ -269,73 +295,73 @@ And <b>html</b> & "quotes".</pre> | |||||
| 269 | </li> | 295 | </li> | ||
| 270 | </ol> | 296 | </ol> | ||
| 271 | </section> | 297 | </section> | ||
| 272 | - | </details> | |||
| 273 | </aside> | 298 | </aside> | ||
| 274 | </div> | 299 | </div> | ||
| 275 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 300 | + | <footer class="foot"> |
| 301 | + | <span>ermite devel</span> | |||
| 302 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 303 | + | <span class="foot-keys"> | |||
| 304 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 305 | + | </footer> | |||
| 306 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 307 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 308 | + | </span> | |||
| 309 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 310 | + | </span> | |||
| 311 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 312 | + | </span> | |||
| 313 | + | </div> | |||
| 314 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 276 | <div class="panel-head"> | 315 | <div class="panel-head"> | ||
| 277 | - | <span>keys</span> | 316 | + | <h2>Keyboard shortcuts</h2> |
| 278 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 317 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 318 | + | <kbd>esc</kbd> close</button> | |||
| 279 | </div> | 319 | </div> | ||
| 280 | <dl class="keys"> | 320 | <dl class="keys"> | ||
| 281 | <div> | 321 | <div> | ||
| 282 | <dt> | 322 | <dt> | ||
| 283 | - | <kbd>j</kbd> | 323 | + | <kbd>ctrl k</kbd> |
| 284 | - | <kbd>k</kbd> | 324 | + | <kbd>:</kbd> |
| 285 | </dt> | 325 | </dt> | ||
| 286 | - | <dd>move cursor down / up</dd> | 326 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 287 | </div> | 327 | </div> | ||
| 288 | <div> | 328 | <div> | ||
| 289 | <dt> | 329 | <dt> | ||
| 290 | - | <kbd>enter</kbd> | 330 | + | <kbd>?</kbd> |
| 291 | </dt> | 331 | </dt> | ||
| 292 | - | <dd>open row under cursor</dd> | 332 | + | <dd>this list</dd> |
| 293 | </div> | 333 | </div> | ||
| 294 | <div> | 334 | <div> | ||
| 295 | <dt> | 335 | <dt> | ||
| 296 | - | <kbd>g</kbd> | 336 | + | <kbd>j</kbd> |
| 297 | - | <kbd>g</kbd> / <kbd>G</kbd> | 337 | + | <kbd>k</kbd> |
| 298 | </dt> | 338 | </dt> | ||
| 299 | - | <dd>first / last row</dd> | 339 | + | <dd>move the row cursor down / up</dd> |
| 300 | </div> | 340 | </div> | ||
| 301 | <div> | 341 | <div> | ||
| 302 | <dt> | 342 | <dt> | ||
| 303 | - | <kbd>/</kbd> | 343 | + | <kbd>enter</kbd> |
| 304 | </dt> | 344 | </dt> | ||
| 305 | - | <dd>focus the filter</dd> | 345 | + | <dd>open the row under the cursor</dd> |
| 306 | </div> | 346 | </div> | ||
| 307 | <div> | 347 | <div> | ||
| 308 | <dt> | 348 | <dt> | ||
| 309 | - | <kbd>g h</kbd> | 349 | + | <kbd>g g</kbd> |
| 310 | - | <kbd>g t</kbd> | 350 | + | <kbd>G</kbd> |
| 311 | </dt> | 351 | </dt> | ||
| 312 | - | <dd>go home / tree</dd> | 352 | + | <dd>first / last row</dd> |
| 313 | </div> | 353 | </div> | ||
| 314 | <div> | 354 | <div> | ||
| 315 | <dt> | 355 | <dt> | ||
| 316 | - | <kbd>g c</kbd> | 356 | + | <kbd>/</kbd> |
| 317 | - | <kbd>g r</kbd> | |||
| 318 | </dt> | 357 | </dt> | ||
| 319 | - | <dd>go commits / refs</dd> | 358 | + | <dd>filter the list</dd> |
| 320 | </div> | 359 | </div> | ||
| 321 | <div> | 360 | <div> | ||
| 322 | <dt> | 361 | <dt> | ||
| 323 | <kbd>b</kbd> | 362 | <kbd>b</kbd> | ||
| 324 | </dt> | 363 | </dt> | ||
| 325 | - | <dd>switch ref (branches, tags)</dd> | 364 | + | <dd>switch branch or tag</dd> |
| 326 | - | </div> | |||
| 327 | - | <div> | |||
| 328 | - | <dt> | |||
| 329 | - | <kbd>:</kbd> | |||
| 330 | - | <kbd>f</kbd> | |||
| 331 | - | </dt> | |||
| 332 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 333 | - | </div> | |||
| 334 | - | <div> | |||
| 335 | - | <dt> | |||
| 336 | - | <kbd>y</kbd> | |||
| 337 | - | </dt> | |||
| 338 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 339 | </div> | 365 | </div> | ||
| 340 | <div> | 366 | <div> | ||
| 341 | <dt> | 367 | <dt> | ||
| @@ -343,15 +369,21 @@ And <b>html</b> & "quotes".</pre> | |||||
| 343 | <kbd>t</kbd> | 369 | <kbd>t</kbd> | ||
| 344 | <kbd>c</kbd> | 370 | <kbd>c</kbd> | ||
| 345 | <kbd>r</kbd> | 371 | <kbd>r</kbd> | ||
| 372 | + | <kbd>m</kbd> | |||
| 346 | </dt> | 373 | </dt> | ||
| 347 | - | <dd>repo sections (when shown)</dd> | 374 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 348 | </div> | 375 | </div> | ||
| 349 | <div> | 376 | <div> | ||
| 350 | <dt> | 377 | <dt> | ||
| 351 | - | <kbd>m</kbd> | 378 | + | <kbd>g h</kbd> |
| 352 | - | <kbd>g m</kbd> | 379 | + | </dt> |
| 380 | + | <dd>home</dd> | |||
| 381 | + | </div> | |||
| 382 | + | <div> | |||
| 383 | + | <dt> | |||
| 384 | + | <kbd>y</kbd> | |||
| 353 | </dt> | 385 | </dt> | ||
| 354 | - | <dd>merge requests (when shown)</dd> | 386 | + | <dd>copy a permalink pinned to the commit</dd> |
| 355 | </div> | 387 | </div> | ||
| 356 | <div> | 388 | <div> | ||
| 357 | <dt> | 389 | <dt> | ||
| @@ -371,19 +403,21 @@ And <b>html</b> & "quotes".</pre> | |||||
| 371 | <dt> | 403 | <dt> | ||
| 372 | <kbd>esc</kbd> | 404 | <kbd>esc</kbd> | ||
| 373 | </dt> | 405 | </dt> | ||
| 374 | - | <dd>close panel</dd> | 406 | + | <dd>close a dialog, leave a field</dd> |
| 375 | </div> | 407 | </div> | ||
| 376 | </dl> | 408 | </dl> | ||
| 377 | </dialog> | 409 | </dialog> | ||
| 378 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 410 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 379 | <div class="panel-head"> | 411 | <div class="panel-head"> | ||
| 380 | - | <label for="refs-filter">ref></label> | 412 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 381 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 413 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 382 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 414 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 415 | + | <kbd>esc</kbd> | |||
| 416 | + | </button> | |||
| 383 | </div> | 417 | </div> | ||
| 384 | <div class="panel-cols" data-picklist> | 418 | <div class="panel-cols" data-picklist> | ||
| 385 | <section aria-label="Branches"> | 419 | <section aria-label="Branches"> | ||
| 386 | - | <h2>branches <span class="mute">2</span> | 420 | + | <h2>Branches <span class="count">2</span> |
| 387 | </h2> | 421 | </h2> | ||
| 388 | <ul> | 422 | <ul> | ||
| 389 | <li> | 423 | <li> | ||
| @@ -397,7 +431,7 @@ And <b>html</b> & "quotes".</pre> | |||||
| 397 | </ul> | 431 | </ul> | ||
| 398 | </section> | 432 | </section> | ||
| 399 | <section aria-label="Tags"> | 433 | <section aria-label="Tags"> | ||
| 400 | - | <h2>tags <span class="mute">1</span> | 434 | + | <h2>Tags <span class="count">1</span> |
| 401 | </h2> | 435 | </h2> | ||
| 402 | <ul> | 436 | <ul> | ||
| 403 | <li> | 437 | <li> | ||
| @@ -408,105 +442,124 @@ And <b>html</b> & "quotes".</pre> | |||||
| 408 | </section> | 442 | </section> | ||
| 409 | </div> | 443 | </div> | ||
| 410 | </dialog> | 444 | </dialog> | ||
| 411 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 445 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 412 | <div class="panel-head"> | 446 | <div class="panel-head"> | ||
| 413 | - | <label for="cmd-q" class="hi">:</label> | 447 | + | <label for="cmd-q" class="sr">go to</label> |
| 414 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 448 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 415 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 449 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 416 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 450 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 417 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 451 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 418 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 452 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 419 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 453 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 420 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 454 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 421 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 455 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 456 | + | <kbd>esc</kbd> | |||
| 457 | + | </button> | |||
| 422 | </div> | 458 | </div> | ||
| 423 | - | <ul id="cmd-list" class="cmd-list"> | 459 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 424 | <li data-k="go home" data-cursor> | 460 | <li data-k="go home" data-cursor> | ||
| 425 | <a href="/" tabindex="-1"> | 461 | <a href="/" tabindex="-1"> | ||
| 426 | - | <span class="k">go</span>home</a> | 462 | + | <span class="lbl">home</span> |
| 463 | + | <span class="k">go</span> | |||
| 464 | + | </a> | |||
| 427 | </li> | 465 | </li> | ||
| 428 | <li data-k="section summary"> | 466 | <li data-k="section summary"> | ||
| 429 | <a href="/diffs" tabindex="-1"> | 467 | <a href="/diffs" tabindex="-1"> | ||
| 430 | - | <span class="k">section</span>summary</a> | 468 | + | <span class="lbl">summary</span> |
| 469 | + | <span class="k">section</span> | |||
| 470 | + | </a> | |||
| 431 | </li> | 471 | </li> | ||
| 432 | - | <li data-k="section tree"> | 472 | + | <li data-k="section files"> |
| 433 | <a href="/diffs/tree/main" tabindex="-1"> | 473 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 434 | - | <span class="k">section</span>tree</a> | 474 | + | <span class="lbl">files</span> |
| 475 | + | <span class="k">section</span> | |||
| 476 | + | </a> | |||
| 435 | </li> | 477 | </li> | ||
| 436 | <li data-k="section commits"> | 478 | <li data-k="section commits"> | ||
| 437 | <a href="/diffs/commits/main" tabindex="-1"> | 479 | <a href="/diffs/commits/main" tabindex="-1"> | ||
| 438 | - | <span class="k">section</span>commits</a> | 480 | + | <span class="lbl">commits</span> |
| 481 | + | <span class="k">section</span> | |||
| 482 | + | </a> | |||
| 439 | </li> | 483 | </li> | ||
| 440 | - | <li data-k="section refs"> | 484 | + | <li data-k="section branches & tags"> |
| 441 | <a href="/diffs/refs" tabindex="-1"> | 485 | <a href="/diffs/refs" tabindex="-1"> | ||
| 442 | - | <span class="k">section</span>refs</a> | 486 | + | <span class="lbl">branches & tags</span> |
| 487 | + | <span class="k">section</span> | |||
| 488 | + | </a> | |||
| 443 | </li> | 489 | </li> | ||
| 444 | <li data-k="section merge requests"> | 490 | <li data-k="section merge requests"> | ||
| 445 | <a href="/diffs/merge-requests" tabindex="-1"> | 491 | <a href="/diffs/merge-requests" tabindex="-1"> | ||
| 446 | - | <span class="k">section</span>merge requests</a> | 492 | + | <span class="lbl">merge requests</span> |
| 493 | + | <span class="k">section</span> | |||
| 494 | + | </a> | |||
| 447 | </li> | 495 | </li> | ||
| 448 | <li data-k="branch main"> | 496 | <li data-k="branch main"> | ||
| 449 | <a href="/diffs/tree/main" tabindex="-1"> | 497 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 450 | - | <span class="k">branch</span>main</a> | 498 | + | <span class="lbl">main</span> |
| 499 | + | <span class="k">branch</span> | |||
| 500 | + | </a> | |||
| 451 | </li> | 501 | </li> | ||
| 452 | <li data-k="branch side"> | 502 | <li data-k="branch side"> | ||
| 453 | <a href="/diffs/tree/side" tabindex="-1"> | 503 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 454 | - | <span class="k">branch</span>side</a> | 504 | + | <span class="lbl">side</span> |
| 505 | + | <span class="k">branch</span> | |||
| 506 | + | </a> | |||
| 455 | </li> | 507 | </li> | ||
| 456 | <li data-k="tag v1.0.0"> | 508 | <li data-k="tag v1.0.0"> | ||
| 457 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 509 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 458 | - | <span class="k">tag</span>v1.0.0</a> | 510 | + | <span class="lbl">v1.0.0</span> |
| 511 | + | <span class="k">tag</span> | |||
| 512 | + | </a> | |||
| 459 | </li> | 513 | </li> | ||
| 460 | </ul> | 514 | </ul> | ||
| 515 | + | <p class="panel-foot"> | |||
| 516 | + | <span> | |||
| 517 | + | <kbd>↑</kbd> | |||
| 518 | + | <kbd>↓</kbd> move</span> | |||
| 519 | + | <span> | |||
| 520 | + | <kbd>enter</kbd> open</span> | |||
| 521 | + | <span> | |||
| 522 | + | <kbd>esc</kbd> close</span> | |||
| 523 | + | </p> | |||
| 461 | </dialog> | 524 | </dialog> | ||
| 462 | - | <footer class="bar status" aria-label="Status"> | 525 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 463 | - | <span class="mode">commit</span> | |||
| 464 | - | <span class="st-ctx">SHA7 · 2 files · +2 -1</span> | |||
| 465 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 466 | - | <span class="hi">:</span> go to…</button> | |||
| 467 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 468 | - | </span> | |||
| 469 | - | <span class="st-hints">d split · u unified · x collapse all · ? help</span> | |||
| 470 | - | <span class="st-src"> | |||
| 471 | - | <span class="st-ver">ermite devel · </span> | |||
| 472 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 473 | - | </span> | |||
| 474 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 475 | - | </footer> | |||
| 476 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 477 | if (q('dialog[open]').count) return; | 526 | if (q('dialog[open]').count) return; | ||
| 478 | let rows = q('[data-rows] > :not([hidden])').arr(); | 527 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 479 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 528 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 480 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 529 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 481 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 530 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 482 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 531 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 532 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 533 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 483 | })(1); | 534 | })(1); | ||
| 484 | - | k-k from:body -> ((d) => { | 535 | + | k-k from:body -> (async (d) => { |
| 485 | if (q('dialog[open]').count) return; | 536 | if (q('dialog[open]').count) return; | ||
| 486 | let rows = q('[data-rows] > :not([hidden])').arr(); | 537 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 487 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 538 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 488 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 539 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 489 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 540 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 490 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 541 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 542 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 543 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 491 | })(-1); | 544 | })(-1); | ||
| 492 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 545 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 493 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 546 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 494 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 547 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 495 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 548 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 496 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 549 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 497 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 550 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 498 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 551 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 499 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 552 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 500 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 553 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 501 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 554 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 502 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 555 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 503 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 556 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 504 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 557 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 505 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 558 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 506 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 559 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 507 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 560 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 508 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 561 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 509 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 562 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 510 | k-next from:body -> { | 563 | k-next from:body -> { | ||
| 511 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 564 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 512 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 565 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commit-rename.html +164 -113
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>SHA7 rename a file and delete another · diffs · example.test</title> | 7 | <title>SHA7 rename a file and delete another · diffs · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,30 +39,49 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">detached</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/diffs" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <header class="show"> | 85 | <header class="show"> | ||
| 63 | <h1>rename a file and delete another</h1> | 86 | <h1>rename a file and delete another</h1> | ||
| 64 | <dl class="kv show-kv"> | 87 | <dl class="kv show-kv"> | ||
| @@ -78,9 +101,12 @@ | |||||
| 78 | </span> | 101 | </span> | ||
| 79 | <span class="grow"> | 102 | <span class="grow"> | ||
| 80 | </span> | 103 | </span> | ||
| 81 | - | <a href="/diffs/commit/SHA40?view=unified" class="vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">[u]nified</a> | 104 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">Unified<kbd>u</kbd> |
| 82 | - | <a href="/diffs/commit/SHA40?view=split" class="vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">[d] split</a> | 105 | + | </a> |
| 83 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 106 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">Split<kbd>d</kbd> |
| 107 | + | </a> | |||
| 108 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd> | |||
| 109 | + | </button> | |||
| 84 | </div> | 110 | </div> | ||
| 85 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 111 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | ||
| 86 | <details class="dfile" id="f1" data-file> | 112 | <details class="dfile" id="f1" data-file> | ||
| @@ -138,10 +164,8 @@ | |||||
| 138 | </div> | 164 | </div> | ||
| 139 | </main> | 165 | </main> | ||
| 140 | <aside class="rail" aria-label="Context"> | 166 | <aside class="rail" aria-label="Context"> | ||
| 141 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 142 | - | <summary>2 changed files</summary> | |||
| 143 | <section aria-labelledby="r-files"> | 167 | <section aria-labelledby="r-files"> | ||
| 144 | - | <h2 id="r-files">files <span>2</span> | 168 | + | <h2 id="r-files">Files <span class="count">2</span> |
| 145 | </h2> | 169 | </h2> | ||
| 146 | <ol class="files" aria-label="Changed files"> | 170 | <ol class="files" aria-label="Changed files"> | ||
| 147 | <li> | 171 | <li> | ||
| @@ -176,73 +200,73 @@ | |||||
| 176 | </li> | 200 | </li> | ||
| 177 | </ol> | 201 | </ol> | ||
| 178 | </section> | 202 | </section> | ||
| 179 | - | </details> | |||
| 180 | </aside> | 203 | </aside> | ||
| 181 | </div> | 204 | </div> | ||
| 182 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 205 | + | <footer class="foot"> |
| 206 | + | <span>ermite devel</span> | |||
| 207 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 208 | + | <span class="foot-keys"> | |||
| 209 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 210 | + | </footer> | |||
| 211 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 212 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 213 | + | </span> | |||
| 214 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 215 | + | </span> | |||
| 216 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 217 | + | </span> | |||
| 218 | + | </div> | |||
| 219 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 183 | <div class="panel-head"> | 220 | <div class="panel-head"> | ||
| 184 | - | <span>keys</span> | 221 | + | <h2>Keyboard shortcuts</h2> |
| 185 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 222 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 223 | + | <kbd>esc</kbd> close</button> | |||
| 186 | </div> | 224 | </div> | ||
| 187 | <dl class="keys"> | 225 | <dl class="keys"> | ||
| 188 | <div> | 226 | <div> | ||
| 189 | <dt> | 227 | <dt> | ||
| 190 | - | <kbd>j</kbd> | 228 | + | <kbd>ctrl k</kbd> |
| 191 | - | <kbd>k</kbd> | 229 | + | <kbd>:</kbd> |
| 192 | </dt> | 230 | </dt> | ||
| 193 | - | <dd>move cursor down / up</dd> | 231 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 194 | </div> | 232 | </div> | ||
| 195 | <div> | 233 | <div> | ||
| 196 | <dt> | 234 | <dt> | ||
| 197 | - | <kbd>enter</kbd> | 235 | + | <kbd>?</kbd> |
| 198 | </dt> | 236 | </dt> | ||
| 199 | - | <dd>open row under cursor</dd> | 237 | + | <dd>this list</dd> |
| 200 | </div> | 238 | </div> | ||
| 201 | <div> | 239 | <div> | ||
| 202 | <dt> | 240 | <dt> | ||
| 203 | - | <kbd>g</kbd> | 241 | + | <kbd>j</kbd> |
| 204 | - | <kbd>g</kbd> / <kbd>G</kbd> | 242 | + | <kbd>k</kbd> |
| 205 | </dt> | 243 | </dt> | ||
| 206 | - | <dd>first / last row</dd> | 244 | + | <dd>move the row cursor down / up</dd> |
| 207 | </div> | 245 | </div> | ||
| 208 | <div> | 246 | <div> | ||
| 209 | <dt> | 247 | <dt> | ||
| 210 | - | <kbd>/</kbd> | 248 | + | <kbd>enter</kbd> |
| 211 | </dt> | 249 | </dt> | ||
| 212 | - | <dd>focus the filter</dd> | 250 | + | <dd>open the row under the cursor</dd> |
| 213 | </div> | 251 | </div> | ||
| 214 | <div> | 252 | <div> | ||
| 215 | <dt> | 253 | <dt> | ||
| 216 | - | <kbd>g h</kbd> | 254 | + | <kbd>g g</kbd> |
| 217 | - | <kbd>g t</kbd> | 255 | + | <kbd>G</kbd> |
| 218 | </dt> | 256 | </dt> | ||
| 219 | - | <dd>go home / tree</dd> | 257 | + | <dd>first / last row</dd> |
| 220 | </div> | 258 | </div> | ||
| 221 | <div> | 259 | <div> | ||
| 222 | <dt> | 260 | <dt> | ||
| 223 | - | <kbd>g c</kbd> | 261 | + | <kbd>/</kbd> |
| 224 | - | <kbd>g r</kbd> | |||
| 225 | </dt> | 262 | </dt> | ||
| 226 | - | <dd>go commits / refs</dd> | 263 | + | <dd>filter the list</dd> |
| 227 | </div> | 264 | </div> | ||
| 228 | <div> | 265 | <div> | ||
| 229 | <dt> | 266 | <dt> | ||
| 230 | <kbd>b</kbd> | 267 | <kbd>b</kbd> | ||
| 231 | </dt> | 268 | </dt> | ||
| 232 | - | <dd>switch ref (branches, tags)</dd> | 269 | + | <dd>switch branch or tag</dd> |
| 233 | - | </div> | |||
| 234 | - | <div> | |||
| 235 | - | <dt> | |||
| 236 | - | <kbd>:</kbd> | |||
| 237 | - | <kbd>f</kbd> | |||
| 238 | - | </dt> | |||
| 239 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 240 | - | </div> | |||
| 241 | - | <div> | |||
| 242 | - | <dt> | |||
| 243 | - | <kbd>y</kbd> | |||
| 244 | - | </dt> | |||
| 245 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 246 | </div> | 270 | </div> | ||
| 247 | <div> | 271 | <div> | ||
| 248 | <dt> | 272 | <dt> | ||
| @@ -250,15 +274,21 @@ | |||||
| 250 | <kbd>t</kbd> | 274 | <kbd>t</kbd> | ||
| 251 | <kbd>c</kbd> | 275 | <kbd>c</kbd> | ||
| 252 | <kbd>r</kbd> | 276 | <kbd>r</kbd> | ||
| 277 | + | <kbd>m</kbd> | |||
| 253 | </dt> | 278 | </dt> | ||
| 254 | - | <dd>repo sections (when shown)</dd> | 279 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 255 | </div> | 280 | </div> | ||
| 256 | <div> | 281 | <div> | ||
| 257 | <dt> | 282 | <dt> | ||
| 258 | - | <kbd>m</kbd> | 283 | + | <kbd>g h</kbd> |
| 259 | - | <kbd>g m</kbd> | 284 | + | </dt> |
| 285 | + | <dd>home</dd> | |||
| 286 | + | </div> | |||
| 287 | + | <div> | |||
| 288 | + | <dt> | |||
| 289 | + | <kbd>y</kbd> | |||
| 260 | </dt> | 290 | </dt> | ||
| 261 | - | <dd>merge requests (when shown)</dd> | 291 | + | <dd>copy a permalink pinned to the commit</dd> |
| 262 | </div> | 292 | </div> | ||
| 263 | <div> | 293 | <div> | ||
| 264 | <dt> | 294 | <dt> | ||
| @@ -278,19 +308,21 @@ | |||||
| 278 | <dt> | 308 | <dt> | ||
| 279 | <kbd>esc</kbd> | 309 | <kbd>esc</kbd> | ||
| 280 | </dt> | 310 | </dt> | ||
| 281 | - | <dd>close panel</dd> | 311 | + | <dd>close a dialog, leave a field</dd> |
| 282 | </div> | 312 | </div> | ||
| 283 | </dl> | 313 | </dl> | ||
| 284 | </dialog> | 314 | </dialog> | ||
| 285 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 315 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 286 | <div class="panel-head"> | 316 | <div class="panel-head"> | ||
| 287 | - | <label for="refs-filter">ref></label> | 317 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 288 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 318 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 289 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 319 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 320 | + | <kbd>esc</kbd> | |||
| 321 | + | </button> | |||
| 290 | </div> | 322 | </div> | ||
| 291 | <div class="panel-cols" data-picklist> | 323 | <div class="panel-cols" data-picklist> | ||
| 292 | <section aria-label="Branches"> | 324 | <section aria-label="Branches"> | ||
| 293 | - | <h2>branches <span class="mute">2</span> | 325 | + | <h2>Branches <span class="count">2</span> |
| 294 | </h2> | 326 | </h2> | ||
| 295 | <ul> | 327 | <ul> | ||
| 296 | <li> | 328 | <li> | ||
| @@ -304,7 +336,7 @@ | |||||
| 304 | </ul> | 336 | </ul> | ||
| 305 | </section> | 337 | </section> | ||
| 306 | <section aria-label="Tags"> | 338 | <section aria-label="Tags"> | ||
| 307 | - | <h2>tags <span class="mute">1</span> | 339 | + | <h2>Tags <span class="count">1</span> |
| 308 | </h2> | 340 | </h2> | ||
| 309 | <ul> | 341 | <ul> | ||
| 310 | <li> | 342 | <li> | ||
| @@ -315,105 +347,124 @@ | |||||
| 315 | </section> | 347 | </section> | ||
| 316 | </div> | 348 | </div> | ||
| 317 | </dialog> | 349 | </dialog> | ||
| 318 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 350 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 319 | <div class="panel-head"> | 351 | <div class="panel-head"> | ||
| 320 | - | <label for="cmd-q" class="hi">:</label> | 352 | + | <label for="cmd-q" class="sr">go to</label> |
| 321 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 353 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 322 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 354 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 323 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 355 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 324 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 356 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 325 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 357 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 326 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 358 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 327 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 359 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 328 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 360 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 361 | + | <kbd>esc</kbd> | |||
| 362 | + | </button> | |||
| 329 | </div> | 363 | </div> | ||
| 330 | - | <ul id="cmd-list" class="cmd-list"> | 364 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 331 | <li data-k="go home" data-cursor> | 365 | <li data-k="go home" data-cursor> | ||
| 332 | <a href="/" tabindex="-1"> | 366 | <a href="/" tabindex="-1"> | ||
| 333 | - | <span class="k">go</span>home</a> | 367 | + | <span class="lbl">home</span> |
| 368 | + | <span class="k">go</span> | |||
| 369 | + | </a> | |||
| 334 | </li> | 370 | </li> | ||
| 335 | <li data-k="section summary"> | 371 | <li data-k="section summary"> | ||
| 336 | <a href="/diffs" tabindex="-1"> | 372 | <a href="/diffs" tabindex="-1"> | ||
| 337 | - | <span class="k">section</span>summary</a> | 373 | + | <span class="lbl">summary</span> |
| 374 | + | <span class="k">section</span> | |||
| 375 | + | </a> | |||
| 338 | </li> | 376 | </li> | ||
| 339 | - | <li data-k="section tree"> | 377 | + | <li data-k="section files"> |
| 340 | <a href="/diffs/tree/main" tabindex="-1"> | 378 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 341 | - | <span class="k">section</span>tree</a> | 379 | + | <span class="lbl">files</span> |
| 380 | + | <span class="k">section</span> | |||
| 381 | + | </a> | |||
| 342 | </li> | 382 | </li> | ||
| 343 | <li data-k="section commits"> | 383 | <li data-k="section commits"> | ||
| 344 | <a href="/diffs/commits/main" tabindex="-1"> | 384 | <a href="/diffs/commits/main" tabindex="-1"> | ||
| 345 | - | <span class="k">section</span>commits</a> | 385 | + | <span class="lbl">commits</span> |
| 386 | + | <span class="k">section</span> | |||
| 387 | + | </a> | |||
| 346 | </li> | 388 | </li> | ||
| 347 | - | <li data-k="section refs"> | 389 | + | <li data-k="section branches & tags"> |
| 348 | <a href="/diffs/refs" tabindex="-1"> | 390 | <a href="/diffs/refs" tabindex="-1"> | ||
| 349 | - | <span class="k">section</span>refs</a> | 391 | + | <span class="lbl">branches & tags</span> |
| 392 | + | <span class="k">section</span> | |||
| 393 | + | </a> | |||
| 350 | </li> | 394 | </li> | ||
| 351 | <li data-k="section merge requests"> | 395 | <li data-k="section merge requests"> | ||
| 352 | <a href="/diffs/merge-requests" tabindex="-1"> | 396 | <a href="/diffs/merge-requests" tabindex="-1"> | ||
| 353 | - | <span class="k">section</span>merge requests</a> | 397 | + | <span class="lbl">merge requests</span> |
| 398 | + | <span class="k">section</span> | |||
| 399 | + | </a> | |||
| 354 | </li> | 400 | </li> | ||
| 355 | <li data-k="branch main"> | 401 | <li data-k="branch main"> | ||
| 356 | <a href="/diffs/tree/main" tabindex="-1"> | 402 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 357 | - | <span class="k">branch</span>main</a> | 403 | + | <span class="lbl">main</span> |
| 404 | + | <span class="k">branch</span> | |||
| 405 | + | </a> | |||
| 358 | </li> | 406 | </li> | ||
| 359 | <li data-k="branch side"> | 407 | <li data-k="branch side"> | ||
| 360 | <a href="/diffs/tree/side" tabindex="-1"> | 408 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 361 | - | <span class="k">branch</span>side</a> | 409 | + | <span class="lbl">side</span> |
| 410 | + | <span class="k">branch</span> | |||
| 411 | + | </a> | |||
| 362 | </li> | 412 | </li> | ||
| 363 | <li data-k="tag v1.0.0"> | 413 | <li data-k="tag v1.0.0"> | ||
| 364 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 414 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 365 | - | <span class="k">tag</span>v1.0.0</a> | 415 | + | <span class="lbl">v1.0.0</span> |
| 416 | + | <span class="k">tag</span> | |||
| 417 | + | </a> | |||
| 366 | </li> | 418 | </li> | ||
| 367 | </ul> | 419 | </ul> | ||
| 420 | + | <p class="panel-foot"> | |||
| 421 | + | <span> | |||
| 422 | + | <kbd>↑</kbd> | |||
| 423 | + | <kbd>↓</kbd> move</span> | |||
| 424 | + | <span> | |||
| 425 | + | <kbd>enter</kbd> open</span> | |||
| 426 | + | <span> | |||
| 427 | + | <kbd>esc</kbd> close</span> | |||
| 428 | + | </p> | |||
| 368 | </dialog> | 429 | </dialog> | ||
| 369 | - | <footer class="bar status" aria-label="Status"> | 430 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 370 | - | <span class="mode">commit</span> | |||
| 371 | - | <span class="st-ctx">SHA7 · 2 files · +0 -1</span> | |||
| 372 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 373 | - | <span class="hi">:</span> go to…</button> | |||
| 374 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 375 | - | </span> | |||
| 376 | - | <span class="st-hints">d split · u unified · x collapse all · ? help</span> | |||
| 377 | - | <span class="st-src"> | |||
| 378 | - | <span class="st-ver">ermite devel · </span> | |||
| 379 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 380 | - | </span> | |||
| 381 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 382 | - | </footer> | |||
| 383 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 384 | if (q('dialog[open]').count) return; | 431 | if (q('dialog[open]').count) return; | ||
| 385 | let rows = q('[data-rows] > :not([hidden])').arr(); | 432 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 386 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 433 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 387 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 434 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 388 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 435 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 389 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 436 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 437 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 438 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 390 | })(1); | 439 | })(1); | ||
| 391 | - | k-k from:body -> ((d) => { | 440 | + | k-k from:body -> (async (d) => { |
| 392 | if (q('dialog[open]').count) return; | 441 | if (q('dialog[open]').count) return; | ||
| 393 | let rows = q('[data-rows] > :not([hidden])').arr(); | 442 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 394 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 443 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 395 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 444 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 396 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 445 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 397 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 446 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 447 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 448 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 398 | })(-1); | 449 | })(-1); | ||
| 399 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 450 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 400 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 451 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 401 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 452 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 402 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 453 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 403 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 454 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 404 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 455 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 405 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 456 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 406 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 457 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 407 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 458 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 408 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 459 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 409 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 460 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 410 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 461 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 411 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 462 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 412 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 463 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 413 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 464 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 414 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 465 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 415 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 466 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 416 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 467 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 417 | k-next from:body -> { | 468 | k-next from:body -> { | ||
| 418 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 469 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 419 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 470 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commit-root.html +164 -113
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>SHA7 root: the first files · diffs · example.test</title> | 7 | <title>SHA7 root: the first files · diffs · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,30 +39,49 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">detached</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/diffs" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <header class="show"> | 85 | <header class="show"> | ||
| 63 | <h1>root: the first files</h1> | 86 | <h1>root: the first files</h1> | ||
| 64 | <dl class="kv show-kv"> | 87 | <dl class="kv show-kv"> | ||
| @@ -76,9 +99,12 @@ | |||||
| 76 | </span> | 99 | </span> | ||
| 77 | <span class="grow"> | 100 | <span class="grow"> | ||
| 78 | </span> | 101 | </span> | ||
| 79 | - | <a href="/diffs/commit/SHA40?view=unified" class="vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">[u]nified</a> | 102 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">Unified<kbd>u</kbd> |
| 80 | - | <a href="/diffs/commit/SHA40?view=split" class="vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">[d] split</a> | 103 | + | </a> |
| 81 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 104 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">Split<kbd>d</kbd> |
| 105 | + | </a> | |||
| 106 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd> | |||
| 107 | + | </button> | |||
| 82 | </div> | 108 | </div> | ||
| 83 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 109 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | ||
| 84 | <details class="dfile" id="f1" data-file open> | 110 | <details class="dfile" id="f1" data-file open> | ||
| @@ -388,10 +414,8 @@ | |||||
| 388 | </div> | 414 | </div> | ||
| 389 | </main> | 415 | </main> | ||
| 390 | <aside class="rail" aria-label="Context"> | 416 | <aside class="rail" aria-label="Context"> | ||
| 391 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 392 | - | <summary>5 changed files</summary> | |||
| 393 | <section aria-labelledby="r-files"> | 417 | <section aria-labelledby="r-files"> | ||
| 394 | - | <h2 id="r-files">files <span>5</span> | 418 | + | <h2 id="r-files">Files <span class="count">5</span> |
| 395 | </h2> | 419 | </h2> | ||
| 396 | <ol class="files" aria-label="Changed files"> | 420 | <ol class="files" aria-label="Changed files"> | ||
| 397 | <li> | 421 | <li> | ||
| @@ -468,73 +492,73 @@ | |||||
| 468 | </li> | 492 | </li> | ||
| 469 | </ol> | 493 | </ol> | ||
| 470 | </section> | 494 | </section> | ||
| 471 | - | </details> | |||
| 472 | </aside> | 495 | </aside> | ||
| 473 | </div> | 496 | </div> | ||
| 474 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 497 | + | <footer class="foot"> |
| 498 | + | <span>ermite devel</span> | |||
| 499 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 500 | + | <span class="foot-keys"> | |||
| 501 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 502 | + | </footer> | |||
| 503 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 504 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 505 | + | </span> | |||
| 506 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 507 | + | </span> | |||
| 508 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 509 | + | </span> | |||
| 510 | + | </div> | |||
| 511 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 475 | <div class="panel-head"> | 512 | <div class="panel-head"> | ||
| 476 | - | <span>keys</span> | 513 | + | <h2>Keyboard shortcuts</h2> |
| 477 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 514 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 515 | + | <kbd>esc</kbd> close</button> | |||
| 478 | </div> | 516 | </div> | ||
| 479 | <dl class="keys"> | 517 | <dl class="keys"> | ||
| 480 | <div> | 518 | <div> | ||
| 481 | <dt> | 519 | <dt> | ||
| 482 | - | <kbd>j</kbd> | 520 | + | <kbd>ctrl k</kbd> |
| 483 | - | <kbd>k</kbd> | 521 | + | <kbd>:</kbd> |
| 484 | </dt> | 522 | </dt> | ||
| 485 | - | <dd>move cursor down / up</dd> | 523 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 486 | </div> | 524 | </div> | ||
| 487 | <div> | 525 | <div> | ||
| 488 | <dt> | 526 | <dt> | ||
| 489 | - | <kbd>enter</kbd> | 527 | + | <kbd>?</kbd> |
| 490 | </dt> | 528 | </dt> | ||
| 491 | - | <dd>open row under cursor</dd> | 529 | + | <dd>this list</dd> |
| 492 | </div> | 530 | </div> | ||
| 493 | <div> | 531 | <div> | ||
| 494 | <dt> | 532 | <dt> | ||
| 495 | - | <kbd>g</kbd> | 533 | + | <kbd>j</kbd> |
| 496 | - | <kbd>g</kbd> / <kbd>G</kbd> | 534 | + | <kbd>k</kbd> |
| 497 | </dt> | 535 | </dt> | ||
| 498 | - | <dd>first / last row</dd> | 536 | + | <dd>move the row cursor down / up</dd> |
| 499 | </div> | 537 | </div> | ||
| 500 | <div> | 538 | <div> | ||
| 501 | <dt> | 539 | <dt> | ||
| 502 | - | <kbd>/</kbd> | 540 | + | <kbd>enter</kbd> |
| 503 | </dt> | 541 | </dt> | ||
| 504 | - | <dd>focus the filter</dd> | 542 | + | <dd>open the row under the cursor</dd> |
| 505 | </div> | 543 | </div> | ||
| 506 | <div> | 544 | <div> | ||
| 507 | <dt> | 545 | <dt> | ||
| 508 | - | <kbd>g h</kbd> | 546 | + | <kbd>g g</kbd> |
| 509 | - | <kbd>g t</kbd> | 547 | + | <kbd>G</kbd> |
| 510 | </dt> | 548 | </dt> | ||
| 511 | - | <dd>go home / tree</dd> | 549 | + | <dd>first / last row</dd> |
| 512 | </div> | 550 | </div> | ||
| 513 | <div> | 551 | <div> | ||
| 514 | <dt> | 552 | <dt> | ||
| 515 | - | <kbd>g c</kbd> | 553 | + | <kbd>/</kbd> |
| 516 | - | <kbd>g r</kbd> | |||
| 517 | </dt> | 554 | </dt> | ||
| 518 | - | <dd>go commits / refs</dd> | 555 | + | <dd>filter the list</dd> |
| 519 | </div> | 556 | </div> | ||
| 520 | <div> | 557 | <div> | ||
| 521 | <dt> | 558 | <dt> | ||
| 522 | <kbd>b</kbd> | 559 | <kbd>b</kbd> | ||
| 523 | </dt> | 560 | </dt> | ||
| 524 | - | <dd>switch ref (branches, tags)</dd> | 561 | + | <dd>switch branch or tag</dd> |
| 525 | - | </div> | |||
| 526 | - | <div> | |||
| 527 | - | <dt> | |||
| 528 | - | <kbd>:</kbd> | |||
| 529 | - | <kbd>f</kbd> | |||
| 530 | - | </dt> | |||
| 531 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 532 | - | </div> | |||
| 533 | - | <div> | |||
| 534 | - | <dt> | |||
| 535 | - | <kbd>y</kbd> | |||
| 536 | - | </dt> | |||
| 537 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 538 | </div> | 562 | </div> | ||
| 539 | <div> | 563 | <div> | ||
| 540 | <dt> | 564 | <dt> | ||
| @@ -542,15 +566,21 @@ | |||||
| 542 | <kbd>t</kbd> | 566 | <kbd>t</kbd> | ||
| 543 | <kbd>c</kbd> | 567 | <kbd>c</kbd> | ||
| 544 | <kbd>r</kbd> | 568 | <kbd>r</kbd> | ||
| 569 | + | <kbd>m</kbd> | |||
| 545 | </dt> | 570 | </dt> | ||
| 546 | - | <dd>repo sections (when shown)</dd> | 571 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 547 | </div> | 572 | </div> | ||
| 548 | <div> | 573 | <div> | ||
| 549 | <dt> | 574 | <dt> | ||
| 550 | - | <kbd>m</kbd> | 575 | + | <kbd>g h</kbd> |
| 551 | - | <kbd>g m</kbd> | 576 | + | </dt> |
| 577 | + | <dd>home</dd> | |||
| 578 | + | </div> | |||
| 579 | + | <div> | |||
| 580 | + | <dt> | |||
| 581 | + | <kbd>y</kbd> | |||
| 552 | </dt> | 582 | </dt> | ||
| 553 | - | <dd>merge requests (when shown)</dd> | 583 | + | <dd>copy a permalink pinned to the commit</dd> |
| 554 | </div> | 584 | </div> | ||
| 555 | <div> | 585 | <div> | ||
| 556 | <dt> | 586 | <dt> | ||
| @@ -570,19 +600,21 @@ | |||||
| 570 | <dt> | 600 | <dt> | ||
| 571 | <kbd>esc</kbd> | 601 | <kbd>esc</kbd> | ||
| 572 | </dt> | 602 | </dt> | ||
| 573 | - | <dd>close panel</dd> | 603 | + | <dd>close a dialog, leave a field</dd> |
| 574 | </div> | 604 | </div> | ||
| 575 | </dl> | 605 | </dl> | ||
| 576 | </dialog> | 606 | </dialog> | ||
| 577 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 607 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 578 | <div class="panel-head"> | 608 | <div class="panel-head"> | ||
| 579 | - | <label for="refs-filter">ref></label> | 609 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 580 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 610 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 581 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 611 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 612 | + | <kbd>esc</kbd> | |||
| 613 | + | </button> | |||
| 582 | </div> | 614 | </div> | ||
| 583 | <div class="panel-cols" data-picklist> | 615 | <div class="panel-cols" data-picklist> | ||
| 584 | <section aria-label="Branches"> | 616 | <section aria-label="Branches"> | ||
| 585 | - | <h2>branches <span class="mute">2</span> | 617 | + | <h2>Branches <span class="count">2</span> |
| 586 | </h2> | 618 | </h2> | ||
| 587 | <ul> | 619 | <ul> | ||
| 588 | <li> | 620 | <li> | ||
| @@ -596,7 +628,7 @@ | |||||
| 596 | </ul> | 628 | </ul> | ||
| 597 | </section> | 629 | </section> | ||
| 598 | <section aria-label="Tags"> | 630 | <section aria-label="Tags"> | ||
| 599 | - | <h2>tags <span class="mute">1</span> | 631 | + | <h2>Tags <span class="count">1</span> |
| 600 | </h2> | 632 | </h2> | ||
| 601 | <ul> | 633 | <ul> | ||
| 602 | <li> | 634 | <li> | ||
| @@ -607,105 +639,124 @@ | |||||
| 607 | </section> | 639 | </section> | ||
| 608 | </div> | 640 | </div> | ||
| 609 | </dialog> | 641 | </dialog> | ||
| 610 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 642 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 611 | <div class="panel-head"> | 643 | <div class="panel-head"> | ||
| 612 | - | <label for="cmd-q" class="hi">:</label> | 644 | + | <label for="cmd-q" class="sr">go to</label> |
| 613 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 645 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 614 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 646 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 615 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 647 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 616 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 648 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 617 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 649 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 618 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 650 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 619 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 651 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 620 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 652 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 653 | + | <kbd>esc</kbd> | |||
| 654 | + | </button> | |||
| 621 | </div> | 655 | </div> | ||
| 622 | - | <ul id="cmd-list" class="cmd-list"> | 656 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 623 | <li data-k="go home" data-cursor> | 657 | <li data-k="go home" data-cursor> | ||
| 624 | <a href="/" tabindex="-1"> | 658 | <a href="/" tabindex="-1"> | ||
| 625 | - | <span class="k">go</span>home</a> | 659 | + | <span class="lbl">home</span> |
| 660 | + | <span class="k">go</span> | |||
| 661 | + | </a> | |||
| 626 | </li> | 662 | </li> | ||
| 627 | <li data-k="section summary"> | 663 | <li data-k="section summary"> | ||
| 628 | <a href="/diffs" tabindex="-1"> | 664 | <a href="/diffs" tabindex="-1"> | ||
| 629 | - | <span class="k">section</span>summary</a> | 665 | + | <span class="lbl">summary</span> |
| 666 | + | <span class="k">section</span> | |||
| 667 | + | </a> | |||
| 630 | </li> | 668 | </li> | ||
| 631 | - | <li data-k="section tree"> | 669 | + | <li data-k="section files"> |
| 632 | <a href="/diffs/tree/main" tabindex="-1"> | 670 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 633 | - | <span class="k">section</span>tree</a> | 671 | + | <span class="lbl">files</span> |
| 672 | + | <span class="k">section</span> | |||
| 673 | + | </a> | |||
| 634 | </li> | 674 | </li> | ||
| 635 | <li data-k="section commits"> | 675 | <li data-k="section commits"> | ||
| 636 | <a href="/diffs/commits/main" tabindex="-1"> | 676 | <a href="/diffs/commits/main" tabindex="-1"> | ||
| 637 | - | <span class="k">section</span>commits</a> | 677 | + | <span class="lbl">commits</span> |
| 678 | + | <span class="k">section</span> | |||
| 679 | + | </a> | |||
| 638 | </li> | 680 | </li> | ||
| 639 | - | <li data-k="section refs"> | 681 | + | <li data-k="section branches & tags"> |
| 640 | <a href="/diffs/refs" tabindex="-1"> | 682 | <a href="/diffs/refs" tabindex="-1"> | ||
| 641 | - | <span class="k">section</span>refs</a> | 683 | + | <span class="lbl">branches & tags</span> |
| 684 | + | <span class="k">section</span> | |||
| 685 | + | </a> | |||
| 642 | </li> | 686 | </li> | ||
| 643 | <li data-k="section merge requests"> | 687 | <li data-k="section merge requests"> | ||
| 644 | <a href="/diffs/merge-requests" tabindex="-1"> | 688 | <a href="/diffs/merge-requests" tabindex="-1"> | ||
| 645 | - | <span class="k">section</span>merge requests</a> | 689 | + | <span class="lbl">merge requests</span> |
| 690 | + | <span class="k">section</span> | |||
| 691 | + | </a> | |||
| 646 | </li> | 692 | </li> | ||
| 647 | <li data-k="branch main"> | 693 | <li data-k="branch main"> | ||
| 648 | <a href="/diffs/tree/main" tabindex="-1"> | 694 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 649 | - | <span class="k">branch</span>main</a> | 695 | + | <span class="lbl">main</span> |
| 696 | + | <span class="k">branch</span> | |||
| 697 | + | </a> | |||
| 650 | </li> | 698 | </li> | ||
| 651 | <li data-k="branch side"> | 699 | <li data-k="branch side"> | ||
| 652 | <a href="/diffs/tree/side" tabindex="-1"> | 700 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 653 | - | <span class="k">branch</span>side</a> | 701 | + | <span class="lbl">side</span> |
| 702 | + | <span class="k">branch</span> | |||
| 703 | + | </a> | |||
| 654 | </li> | 704 | </li> | ||
| 655 | <li data-k="tag v1.0.0"> | 705 | <li data-k="tag v1.0.0"> | ||
| 656 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 706 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 657 | - | <span class="k">tag</span>v1.0.0</a> | 707 | + | <span class="lbl">v1.0.0</span> |
| 708 | + | <span class="k">tag</span> | |||
| 709 | + | </a> | |||
| 658 | </li> | 710 | </li> | ||
| 659 | </ul> | 711 | </ul> | ||
| 712 | + | <p class="panel-foot"> | |||
| 713 | + | <span> | |||
| 714 | + | <kbd>↑</kbd> | |||
| 715 | + | <kbd>↓</kbd> move</span> | |||
| 716 | + | <span> | |||
| 717 | + | <kbd>enter</kbd> open</span> | |||
| 718 | + | <span> | |||
| 719 | + | <kbd>esc</kbd> close</span> | |||
| 720 | + | </p> | |||
| 660 | </dialog> | 721 | </dialog> | ||
| 661 | - | <footer class="bar status" aria-label="Status"> | 722 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 662 | - | <span class="mode">commit</span> | |||
| 663 | - | <span class="st-ctx">SHA7 · 5 files · +11 -0</span> | |||
| 664 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 665 | - | <span class="hi">:</span> go to…</button> | |||
| 666 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 667 | - | </span> | |||
| 668 | - | <span class="st-hints">d split · u unified · x collapse all · ? help</span> | |||
| 669 | - | <span class="st-src"> | |||
| 670 | - | <span class="st-ver">ermite devel · </span> | |||
| 671 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 672 | - | </span> | |||
| 673 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 674 | - | </footer> | |||
| 675 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 676 | if (q('dialog[open]').count) return; | 723 | if (q('dialog[open]').count) return; | ||
| 677 | let rows = q('[data-rows] > :not([hidden])').arr(); | 724 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 678 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 725 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 679 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 726 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 680 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 727 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 681 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 728 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 729 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 730 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 682 | })(1); | 731 | })(1); | ||
| 683 | - | k-k from:body -> ((d) => { | 732 | + | k-k from:body -> (async (d) => { |
| 684 | if (q('dialog[open]').count) return; | 733 | if (q('dialog[open]').count) return; | ||
| 685 | let rows = q('[data-rows] > :not([hidden])').arr(); | 734 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 686 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 735 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 687 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 736 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 688 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 737 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 689 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 738 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 739 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 740 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 690 | })(-1); | 741 | })(-1); | ||
| 691 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 742 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 692 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 743 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 693 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 744 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 694 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 745 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 695 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 746 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 696 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 747 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 697 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 748 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 698 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 749 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 699 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 750 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 700 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 751 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 701 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 752 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 702 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 753 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 703 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 754 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 704 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 755 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 705 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 756 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 706 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 757 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 707 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 758 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 708 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 759 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 709 | k-next from:body -> { | 760 | k-next from:body -> { | ||
| 710 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 761 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 711 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 762 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commit-trojan.html +164 -113
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>SHA7 add evil.go · diffs · example.test</title> | 7 | <title>SHA7 add evil.go · diffs · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,30 +39,49 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">detached</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/diffs" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <header class="show"> | 85 | <header class="show"> | ||
| 63 | <h1>add evil.go</h1> | 86 | <h1>add evil.go</h1> | ||
| 64 | <pre class="msg">looks <U+202E>fine<U+202C></pre> | 87 | <pre class="msg">looks <U+202E>fine<U+202C></pre> | ||
| @@ -79,9 +102,12 @@ | |||||
| 79 | </span> | 102 | </span> | ||
| 80 | <span class="grow"> | 103 | <span class="grow"> | ||
| 81 | </span> | 104 | </span> | ||
| 82 | - | <a href="/diffs/commit/SHA40?view=unified" class="vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">[u]nified</a> | 105 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'unified' ? 'true' : false">Unified<kbd>u</kbd> |
| 83 | - | <a href="/diffs/commit/SHA40?view=split" class="vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">[d] split</a> | 106 | + | </a> |
| 84 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 107 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" :aria-current="q('#diffs').attr['data-diff'] === 'split' ? 'true' : false">Split<kbd>d</kbd> |
| 108 | + | </a> | |||
| 109 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd> | |||
| 110 | + | </button> | |||
| 85 | </div> | 111 | </div> | ||
| 86 | <p class="notice">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class="inv"><U+XXXX></span> markers, never applied.</p> | 112 | <p class="notice">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class="inv"><U+XXXX></span> markers, never applied.</p> | ||
| 87 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 113 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | ||
| @@ -262,10 +288,8 @@ | |||||
| 262 | </div> | 288 | </div> | ||
| 263 | </main> | 289 | </main> | ||
| 264 | <aside class="rail" aria-label="Context"> | 290 | <aside class="rail" aria-label="Context"> | ||
| 265 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 266 | - | <summary>1 changed file</summary> | |||
| 267 | <section aria-labelledby="r-files"> | 291 | <section aria-labelledby="r-files"> | ||
| 268 | - | <h2 id="r-files">files <span>1</span> | 292 | + | <h2 id="r-files">Files <span class="count">1</span> |
| 269 | </h2> | 293 | </h2> | ||
| 270 | <ol class="files" aria-label="Changed files"> | 294 | <ol class="files" aria-label="Changed files"> | ||
| 271 | <li> | 295 | <li> | ||
| @@ -285,73 +309,73 @@ | |||||
| 285 | </li> | 309 | </li> | ||
| 286 | </ol> | 310 | </ol> | ||
| 287 | </section> | 311 | </section> | ||
| 288 | - | </details> | |||
| 289 | </aside> | 312 | </aside> | ||
| 290 | </div> | 313 | </div> | ||
| 291 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 314 | + | <footer class="foot"> |
| 315 | + | <span>ermite devel</span> | |||
| 316 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 317 | + | <span class="foot-keys"> | |||
| 318 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 319 | + | </footer> | |||
| 320 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 321 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 322 | + | </span> | |||
| 323 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 324 | + | </span> | |||
| 325 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 326 | + | </span> | |||
| 327 | + | </div> | |||
| 328 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 292 | <div class="panel-head"> | 329 | <div class="panel-head"> | ||
| 293 | - | <span>keys</span> | 330 | + | <h2>Keyboard shortcuts</h2> |
| 294 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 331 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 332 | + | <kbd>esc</kbd> close</button> | |||
| 295 | </div> | 333 | </div> | ||
| 296 | <dl class="keys"> | 334 | <dl class="keys"> | ||
| 297 | <div> | 335 | <div> | ||
| 298 | <dt> | 336 | <dt> | ||
| 299 | - | <kbd>j</kbd> | 337 | + | <kbd>ctrl k</kbd> |
| 300 | - | <kbd>k</kbd> | 338 | + | <kbd>:</kbd> |
| 301 | </dt> | 339 | </dt> | ||
| 302 | - | <dd>move cursor down / up</dd> | 340 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 303 | </div> | 341 | </div> | ||
| 304 | <div> | 342 | <div> | ||
| 305 | <dt> | 343 | <dt> | ||
| 306 | - | <kbd>enter</kbd> | 344 | + | <kbd>?</kbd> |
| 307 | </dt> | 345 | </dt> | ||
| 308 | - | <dd>open row under cursor</dd> | 346 | + | <dd>this list</dd> |
| 309 | </div> | 347 | </div> | ||
| 310 | <div> | 348 | <div> | ||
| 311 | <dt> | 349 | <dt> | ||
| 312 | - | <kbd>g</kbd> | 350 | + | <kbd>j</kbd> |
| 313 | - | <kbd>g</kbd> / <kbd>G</kbd> | 351 | + | <kbd>k</kbd> |
| 314 | </dt> | 352 | </dt> | ||
| 315 | - | <dd>first / last row</dd> | 353 | + | <dd>move the row cursor down / up</dd> |
| 316 | </div> | 354 | </div> | ||
| 317 | <div> | 355 | <div> | ||
| 318 | <dt> | 356 | <dt> | ||
| 319 | - | <kbd>/</kbd> | 357 | + | <kbd>enter</kbd> |
| 320 | </dt> | 358 | </dt> | ||
| 321 | - | <dd>focus the filter</dd> | 359 | + | <dd>open the row under the cursor</dd> |
| 322 | </div> | 360 | </div> | ||
| 323 | <div> | 361 | <div> | ||
| 324 | <dt> | 362 | <dt> | ||
| 325 | - | <kbd>g h</kbd> | 363 | + | <kbd>g g</kbd> |
| 326 | - | <kbd>g t</kbd> | 364 | + | <kbd>G</kbd> |
| 327 | </dt> | 365 | </dt> | ||
| 328 | - | <dd>go home / tree</dd> | 366 | + | <dd>first / last row</dd> |
| 329 | </div> | 367 | </div> | ||
| 330 | <div> | 368 | <div> | ||
| 331 | <dt> | 369 | <dt> | ||
| 332 | - | <kbd>g c</kbd> | 370 | + | <kbd>/</kbd> |
| 333 | - | <kbd>g r</kbd> | |||
| 334 | </dt> | 371 | </dt> | ||
| 335 | - | <dd>go commits / refs</dd> | 372 | + | <dd>filter the list</dd> |
| 336 | </div> | 373 | </div> | ||
| 337 | <div> | 374 | <div> | ||
| 338 | <dt> | 375 | <dt> | ||
| 339 | <kbd>b</kbd> | 376 | <kbd>b</kbd> | ||
| 340 | </dt> | 377 | </dt> | ||
| 341 | - | <dd>switch ref (branches, tags)</dd> | 378 | + | <dd>switch branch or tag</dd> |
| 342 | - | </div> | |||
| 343 | - | <div> | |||
| 344 | - | <dt> | |||
| 345 | - | <kbd>:</kbd> | |||
| 346 | - | <kbd>f</kbd> | |||
| 347 | - | </dt> | |||
| 348 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 349 | - | </div> | |||
| 350 | - | <div> | |||
| 351 | - | <dt> | |||
| 352 | - | <kbd>y</kbd> | |||
| 353 | - | </dt> | |||
| 354 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 355 | </div> | 379 | </div> | ||
| 356 | <div> | 380 | <div> | ||
| 357 | <dt> | 381 | <dt> | ||
| @@ -359,15 +383,21 @@ | |||||
| 359 | <kbd>t</kbd> | 383 | <kbd>t</kbd> | ||
| 360 | <kbd>c</kbd> | 384 | <kbd>c</kbd> | ||
| 361 | <kbd>r</kbd> | 385 | <kbd>r</kbd> | ||
| 386 | + | <kbd>m</kbd> | |||
| 362 | </dt> | 387 | </dt> | ||
| 363 | - | <dd>repo sections (when shown)</dd> | 388 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 364 | </div> | 389 | </div> | ||
| 365 | <div> | 390 | <div> | ||
| 366 | <dt> | 391 | <dt> | ||
| 367 | - | <kbd>m</kbd> | 392 | + | <kbd>g h</kbd> |
| 368 | - | <kbd>g m</kbd> | 393 | + | </dt> |
| 394 | + | <dd>home</dd> | |||
| 395 | + | </div> | |||
| 396 | + | <div> | |||
| 397 | + | <dt> | |||
| 398 | + | <kbd>y</kbd> | |||
| 369 | </dt> | 399 | </dt> | ||
| 370 | - | <dd>merge requests (when shown)</dd> | 400 | + | <dd>copy a permalink pinned to the commit</dd> |
| 371 | </div> | 401 | </div> | ||
| 372 | <div> | 402 | <div> | ||
| 373 | <dt> | 403 | <dt> | ||
| @@ -387,19 +417,21 @@ | |||||
| 387 | <dt> | 417 | <dt> | ||
| 388 | <kbd>esc</kbd> | 418 | <kbd>esc</kbd> | ||
| 389 | </dt> | 419 | </dt> | ||
| 390 | - | <dd>close panel</dd> | 420 | + | <dd>close a dialog, leave a field</dd> |
| 391 | </div> | 421 | </div> | ||
| 392 | </dl> | 422 | </dl> | ||
| 393 | </dialog> | 423 | </dialog> | ||
| 394 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 424 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 395 | <div class="panel-head"> | 425 | <div class="panel-head"> | ||
| 396 | - | <label for="refs-filter">ref></label> | 426 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 397 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 427 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 398 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 428 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 429 | + | <kbd>esc</kbd> | |||
| 430 | + | </button> | |||
| 399 | </div> | 431 | </div> | ||
| 400 | <div class="panel-cols" data-picklist> | 432 | <div class="panel-cols" data-picklist> | ||
| 401 | <section aria-label="Branches"> | 433 | <section aria-label="Branches"> | ||
| 402 | - | <h2>branches <span class="mute">2</span> | 434 | + | <h2>Branches <span class="count">2</span> |
| 403 | </h2> | 435 | </h2> | ||
| 404 | <ul> | 436 | <ul> | ||
| 405 | <li> | 437 | <li> | ||
| @@ -413,7 +445,7 @@ | |||||
| 413 | </ul> | 445 | </ul> | ||
| 414 | </section> | 446 | </section> | ||
| 415 | <section aria-label="Tags"> | 447 | <section aria-label="Tags"> | ||
| 416 | - | <h2>tags <span class="mute">1</span> | 448 | + | <h2>Tags <span class="count">1</span> |
| 417 | </h2> | 449 | </h2> | ||
| 418 | <ul> | 450 | <ul> | ||
| 419 | <li> | 451 | <li> | ||
| @@ -424,105 +456,124 @@ | |||||
| 424 | </section> | 456 | </section> | ||
| 425 | </div> | 457 | </div> | ||
| 426 | </dialog> | 458 | </dialog> | ||
| 427 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 459 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 428 | <div class="panel-head"> | 460 | <div class="panel-head"> | ||
| 429 | - | <label for="cmd-q" class="hi">:</label> | 461 | + | <label for="cmd-q" class="sr">go to</label> |
| 430 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 462 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 431 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 463 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 432 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 464 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 433 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 465 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 434 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 466 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 435 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 467 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 436 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 468 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 437 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 469 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 470 | + | <kbd>esc</kbd> | |||
| 471 | + | </button> | |||
| 438 | </div> | 472 | </div> | ||
| 439 | - | <ul id="cmd-list" class="cmd-list"> | 473 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 440 | <li data-k="go home" data-cursor> | 474 | <li data-k="go home" data-cursor> | ||
| 441 | <a href="/" tabindex="-1"> | 475 | <a href="/" tabindex="-1"> | ||
| 442 | - | <span class="k">go</span>home</a> | 476 | + | <span class="lbl">home</span> |
| 477 | + | <span class="k">go</span> | |||
| 478 | + | </a> | |||
| 443 | </li> | 479 | </li> | ||
| 444 | <li data-k="section summary"> | 480 | <li data-k="section summary"> | ||
| 445 | <a href="/diffs" tabindex="-1"> | 481 | <a href="/diffs" tabindex="-1"> | ||
| 446 | - | <span class="k">section</span>summary</a> | 482 | + | <span class="lbl">summary</span> |
| 483 | + | <span class="k">section</span> | |||
| 484 | + | </a> | |||
| 447 | </li> | 485 | </li> | ||
| 448 | - | <li data-k="section tree"> | 486 | + | <li data-k="section files"> |
| 449 | <a href="/diffs/tree/main" tabindex="-1"> | 487 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 450 | - | <span class="k">section</span>tree</a> | 488 | + | <span class="lbl">files</span> |
| 489 | + | <span class="k">section</span> | |||
| 490 | + | </a> | |||
| 451 | </li> | 491 | </li> | ||
| 452 | <li data-k="section commits"> | 492 | <li data-k="section commits"> | ||
| 453 | <a href="/diffs/commits/main" tabindex="-1"> | 493 | <a href="/diffs/commits/main" tabindex="-1"> | ||
| 454 | - | <span class="k">section</span>commits</a> | 494 | + | <span class="lbl">commits</span> |
| 495 | + | <span class="k">section</span> | |||
| 496 | + | </a> | |||
| 455 | </li> | 497 | </li> | ||
| 456 | - | <li data-k="section refs"> | 498 | + | <li data-k="section branches & tags"> |
| 457 | <a href="/diffs/refs" tabindex="-1"> | 499 | <a href="/diffs/refs" tabindex="-1"> | ||
| 458 | - | <span class="k">section</span>refs</a> | 500 | + | <span class="lbl">branches & tags</span> |
| 501 | + | <span class="k">section</span> | |||
| 502 | + | </a> | |||
| 459 | </li> | 503 | </li> | ||
| 460 | <li data-k="section merge requests"> | 504 | <li data-k="section merge requests"> | ||
| 461 | <a href="/diffs/merge-requests" tabindex="-1"> | 505 | <a href="/diffs/merge-requests" tabindex="-1"> | ||
| 462 | - | <span class="k">section</span>merge requests</a> | 506 | + | <span class="lbl">merge requests</span> |
| 507 | + | <span class="k">section</span> | |||
| 508 | + | </a> | |||
| 463 | </li> | 509 | </li> | ||
| 464 | <li data-k="branch main"> | 510 | <li data-k="branch main"> | ||
| 465 | <a href="/diffs/tree/main" tabindex="-1"> | 511 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 466 | - | <span class="k">branch</span>main</a> | 512 | + | <span class="lbl">main</span> |
| 513 | + | <span class="k">branch</span> | |||
| 514 | + | </a> | |||
| 467 | </li> | 515 | </li> | ||
| 468 | <li data-k="branch side"> | 516 | <li data-k="branch side"> | ||
| 469 | <a href="/diffs/tree/side" tabindex="-1"> | 517 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 470 | - | <span class="k">branch</span>side</a> | 518 | + | <span class="lbl">side</span> |
| 519 | + | <span class="k">branch</span> | |||
| 520 | + | </a> | |||
| 471 | </li> | 521 | </li> | ||
| 472 | <li data-k="tag v1.0.0"> | 522 | <li data-k="tag v1.0.0"> | ||
| 473 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 523 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 474 | - | <span class="k">tag</span>v1.0.0</a> | 524 | + | <span class="lbl">v1.0.0</span> |
| 525 | + | <span class="k">tag</span> | |||
| 526 | + | </a> | |||
| 475 | </li> | 527 | </li> | ||
| 476 | </ul> | 528 | </ul> | ||
| 529 | + | <p class="panel-foot"> | |||
| 530 | + | <span> | |||
| 531 | + | <kbd>↑</kbd> | |||
| 532 | + | <kbd>↓</kbd> move</span> | |||
| 533 | + | <span> | |||
| 534 | + | <kbd>enter</kbd> open</span> | |||
| 535 | + | <span> | |||
| 536 | + | <kbd>esc</kbd> close</span> | |||
| 537 | + | </p> | |||
| 477 | </dialog> | 538 | </dialog> | ||
| 478 | - | <footer class="bar status" aria-label="Status"> | 539 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 479 | - | <span class="mode">commit</span> | |||
| 480 | - | <span class="st-ctx">SHA7 · 1 file · +8 -0</span> | |||
| 481 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 482 | - | <span class="hi">:</span> go to…</button> | |||
| 483 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 484 | - | </span> | |||
| 485 | - | <span class="st-hints">d split · u unified · x collapse all · ? help</span> | |||
| 486 | - | <span class="st-src"> | |||
| 487 | - | <span class="st-ver">ermite devel · </span> | |||
| 488 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 489 | - | </span> | |||
| 490 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 491 | - | </footer> | |||
| 492 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 493 | if (q('dialog[open]').count) return; | 540 | if (q('dialog[open]').count) return; | ||
| 494 | let rows = q('[data-rows] > :not([hidden])').arr(); | 541 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 495 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 542 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 496 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 543 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 497 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 544 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 498 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 545 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 546 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 547 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 499 | })(1); | 548 | })(1); | ||
| 500 | - | k-k from:body -> ((d) => { | 549 | + | k-k from:body -> (async (d) => { |
| 501 | if (q('dialog[open]').count) return; | 550 | if (q('dialog[open]').count) return; | ||
| 502 | let rows = q('[data-rows] > :not([hidden])').arr(); | 551 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 503 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 552 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 504 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 553 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 505 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 554 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 506 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 555 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 556 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 557 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 507 | })(-1); | 558 | })(-1); | ||
| 508 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 559 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 509 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 560 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 510 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 561 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 511 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 562 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 512 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 563 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 513 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 564 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 514 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 565 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 515 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 566 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 516 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 567 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 517 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 568 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 518 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 569 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 519 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 570 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 520 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 571 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 521 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 572 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 522 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 573 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 523 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 574 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 524 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 575 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 525 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 576 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 526 | k-next from:body -> { | 577 | k-next from:body -> { | ||
| 527 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 578 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 528 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 579 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commits-page2.html +160 -115
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>commits · long · example.test</title> | 7 | <title>commits · long · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,33 +39,52 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">main</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/long" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/long" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/long/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/long/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/long/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <div class="filter"> | 85 | <div class="filter"> | ||
| 63 | - | <label for="commit-filter">filter></label> | 86 | + | <label for="commit-filter" class="sr">filter</label> |
| 64 | - | <input id="commit-filter" type="search" data-filter placeholder="subject, sha or author ( / )" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | 87 | + | <input id="commit-filter" type="search" data-filter placeholder="Filter by subject, sha or author…" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); |
| 65 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | 88 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | ||
| 66 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | 89 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | ||
| 67 | </div> | 90 | </div> | ||
| @@ -101,15 +124,13 @@ | |||||
| 101 | </section> | 124 | </section> | ||
| 102 | </div> | 125 | </div> | ||
| 103 | <nav class="pager" aria-label="Pages"> | 126 | <nav class="pager" aria-label="Pages"> | ||
| 104 | - | <a href="/long/commits/main">← newest</a> | 127 | + | <a class="btn" href="/long/commits/main">← Newest</a> |
| 105 | - | <span class="mute">older →</span> | 128 | + | <span class="btn" aria-disabled="true">Older →</span> |
| 106 | </nav> | 129 | </nav> | ||
| 107 | </main> | 130 | </main> | ||
| 108 | <aside class="rail" aria-label="Context"> | 131 | <aside class="rail" aria-label="Context"> | ||
| 109 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 110 | - | <summary>history · refs on this page</summary> | |||
| 111 | <section aria-labelledby="r-hist"> | 132 | <section aria-labelledby="r-hist"> | ||
| 112 | - | <h2 id="r-hist">history</h2> | 133 | + | <h2 id="r-hist">History</h2> |
| 113 | <dl class="kv"> | 134 | <dl class="kv"> | ||
| 114 | <dt>ref</dt> | 135 | <dt>ref</dt> | ||
| 115 | <dd>main</dd> | 136 | <dd>main</dd> | ||
| @@ -118,76 +139,76 @@ | |||||
| 118 | </dl> | 139 | </dl> | ||
| 119 | </section> | 140 | </section> | ||
| 120 | <section aria-labelledby="r-refs"> | 141 | <section aria-labelledby="r-refs"> | ||
| 121 | - | <h2 id="r-refs">refs on this page</h2> | 142 | + | <h2 id="r-refs">Refs on this page</h2> |
| 122 | <p class="dim">no tag or branch head here</p> | 143 | <p class="dim">no tag or branch head here</p> | ||
| 123 | </section> | 144 | </section> | ||
| 124 | - | </details> | |||
| 125 | </aside> | 145 | </aside> | ||
| 126 | </div> | 146 | </div> | ||
| 127 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 147 | + | <footer class="foot"> |
| 148 | + | <span>ermite devel</span> | |||
| 149 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 150 | + | <span class="foot-keys"> | |||
| 151 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 152 | + | </footer> | |||
| 153 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 154 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 155 | + | </span> | |||
| 156 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 157 | + | </span> | |||
| 158 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 159 | + | </span> | |||
| 160 | + | </div> | |||
| 161 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 128 | <div class="panel-head"> | 162 | <div class="panel-head"> | ||
| 129 | - | <span>keys</span> | 163 | + | <h2>Keyboard shortcuts</h2> |
| 130 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 164 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 165 | + | <kbd>esc</kbd> close</button> | |||
| 131 | </div> | 166 | </div> | ||
| 132 | <dl class="keys"> | 167 | <dl class="keys"> | ||
| 133 | <div> | 168 | <div> | ||
| 134 | <dt> | 169 | <dt> | ||
| 135 | - | <kbd>j</kbd> | 170 | + | <kbd>ctrl k</kbd> |
| 136 | - | <kbd>k</kbd> | 171 | + | <kbd>:</kbd> |
| 137 | </dt> | 172 | </dt> | ||
| 138 | - | <dd>move cursor down / up</dd> | 173 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 139 | </div> | 174 | </div> | ||
| 140 | <div> | 175 | <div> | ||
| 141 | <dt> | 176 | <dt> | ||
| 142 | - | <kbd>enter</kbd> | 177 | + | <kbd>?</kbd> |
| 143 | </dt> | 178 | </dt> | ||
| 144 | - | <dd>open row under cursor</dd> | 179 | + | <dd>this list</dd> |
| 145 | </div> | 180 | </div> | ||
| 146 | <div> | 181 | <div> | ||
| 147 | <dt> | 182 | <dt> | ||
| 148 | - | <kbd>g</kbd> | 183 | + | <kbd>j</kbd> |
| 149 | - | <kbd>g</kbd> / <kbd>G</kbd> | 184 | + | <kbd>k</kbd> |
| 150 | </dt> | 185 | </dt> | ||
| 151 | - | <dd>first / last row</dd> | 186 | + | <dd>move the row cursor down / up</dd> |
| 152 | </div> | 187 | </div> | ||
| 153 | <div> | 188 | <div> | ||
| 154 | <dt> | 189 | <dt> | ||
| 155 | - | <kbd>/</kbd> | 190 | + | <kbd>enter</kbd> |
| 156 | </dt> | 191 | </dt> | ||
| 157 | - | <dd>focus the filter</dd> | 192 | + | <dd>open the row under the cursor</dd> |
| 158 | </div> | 193 | </div> | ||
| 159 | <div> | 194 | <div> | ||
| 160 | <dt> | 195 | <dt> | ||
| 161 | - | <kbd>g h</kbd> | 196 | + | <kbd>g g</kbd> |
| 162 | - | <kbd>g t</kbd> | 197 | + | <kbd>G</kbd> |
| 163 | </dt> | 198 | </dt> | ||
| 164 | - | <dd>go home / tree</dd> | 199 | + | <dd>first / last row</dd> |
| 165 | </div> | 200 | </div> | ||
| 166 | <div> | 201 | <div> | ||
| 167 | <dt> | 202 | <dt> | ||
| 168 | - | <kbd>g c</kbd> | 203 | + | <kbd>/</kbd> |
| 169 | - | <kbd>g r</kbd> | |||
| 170 | </dt> | 204 | </dt> | ||
| 171 | - | <dd>go commits / refs</dd> | 205 | + | <dd>filter the list</dd> |
| 172 | </div> | 206 | </div> | ||
| 173 | <div> | 207 | <div> | ||
| 174 | <dt> | 208 | <dt> | ||
| 175 | <kbd>b</kbd> | 209 | <kbd>b</kbd> | ||
| 176 | </dt> | 210 | </dt> | ||
| 177 | - | <dd>switch ref (branches, tags)</dd> | 211 | + | <dd>switch branch or tag</dd> |
| 178 | - | </div> | |||
| 179 | - | <div> | |||
| 180 | - | <dt> | |||
| 181 | - | <kbd>:</kbd> | |||
| 182 | - | <kbd>f</kbd> | |||
| 183 | - | </dt> | |||
| 184 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 185 | - | </div> | |||
| 186 | - | <div> | |||
| 187 | - | <dt> | |||
| 188 | - | <kbd>y</kbd> | |||
| 189 | - | </dt> | |||
| 190 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 191 | </div> | 212 | </div> | ||
| 192 | <div> | 213 | <div> | ||
| 193 | <dt> | 214 | <dt> | ||
| @@ -195,15 +216,21 @@ | |||||
| 195 | <kbd>t</kbd> | 216 | <kbd>t</kbd> | ||
| 196 | <kbd>c</kbd> | 217 | <kbd>c</kbd> | ||
| 197 | <kbd>r</kbd> | 218 | <kbd>r</kbd> | ||
| 219 | + | <kbd>m</kbd> | |||
| 198 | </dt> | 220 | </dt> | ||
| 199 | - | <dd>repo sections (when shown)</dd> | 221 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 200 | </div> | 222 | </div> | ||
| 201 | <div> | 223 | <div> | ||
| 202 | <dt> | 224 | <dt> | ||
| 203 | - | <kbd>m</kbd> | 225 | + | <kbd>g h</kbd> |
| 204 | - | <kbd>g m</kbd> | |||
| 205 | </dt> | 226 | </dt> | ||
| 206 | - | <dd>merge requests (when shown)</dd> | 227 | + | <dd>home</dd> |
| 228 | + | </div> | |||
| 229 | + | <div> | |||
| 230 | + | <dt> | |||
| 231 | + | <kbd>y</kbd> | |||
| 232 | + | </dt> | |||
| 233 | + | <dd>copy a permalink pinned to the commit</dd> | |||
| 207 | </div> | 234 | </div> | ||
| 208 | <div> | 235 | <div> | ||
| 209 | <dt> | 236 | <dt> | ||
| @@ -223,19 +250,21 @@ | |||||
| 223 | <dt> | 250 | <dt> | ||
| 224 | <kbd>esc</kbd> | 251 | <kbd>esc</kbd> | ||
| 225 | </dt> | 252 | </dt> | ||
| 226 | - | <dd>close panel</dd> | 253 | + | <dd>close a dialog, leave a field</dd> |
| 227 | </div> | 254 | </div> | ||
| 228 | </dl> | 255 | </dl> | ||
| 229 | </dialog> | 256 | </dialog> | ||
| 230 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 257 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 231 | <div class="panel-head"> | 258 | <div class="panel-head"> | ||
| 232 | - | <label for="refs-filter">ref></label> | 259 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 233 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 260 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 234 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 261 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 262 | + | <kbd>esc</kbd> | |||
| 263 | + | </button> | |||
| 235 | </div> | 264 | </div> | ||
| 236 | <div class="panel-cols" data-picklist> | 265 | <div class="panel-cols" data-picklist> | ||
| 237 | <section aria-label="Branches"> | 266 | <section aria-label="Branches"> | ||
| 238 | - | <h2>branches <span class="mute">1</span> | 267 | + | <h2>Branches <span class="count">1</span> |
| 239 | </h2> | 268 | </h2> | ||
| 240 | <ul> | 269 | <ul> | ||
| 241 | <li> | 270 | <li> | ||
| @@ -245,109 +274,125 @@ | |||||
| 245 | </ul> | 274 | </ul> | ||
| 246 | </section> | 275 | </section> | ||
| 247 | <section aria-label="Tags"> | 276 | <section aria-label="Tags"> | ||
| 248 | - | <h2>tags <span class="mute">0</span> | 277 | + | <h2>Tags <span class="count">0</span> |
| 249 | </h2> | 278 | </h2> | ||
| 250 | <ul> | 279 | <ul> | ||
| 251 | </ul> | 280 | </ul> | ||
| 252 | </section> | 281 | </section> | ||
| 253 | </div> | 282 | </div> | ||
| 254 | </dialog> | 283 | </dialog> | ||
| 255 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 284 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 256 | <div class="panel-head"> | 285 | <div class="panel-head"> | ||
| 257 | - | <label for="cmd-q" class="hi">:</label> | 286 | + | <label for="cmd-q" class="sr">go to</label> |
| 258 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 287 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 259 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 288 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 260 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 289 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 261 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 290 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 262 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 291 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 263 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 292 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 264 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 293 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 265 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 294 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 295 | + | <kbd>esc</kbd> | |||
| 296 | + | </button> | |||
| 266 | </div> | 297 | </div> | ||
| 267 | - | <ul id="cmd-list" class="cmd-list"> | 298 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 268 | <li data-k="go home" data-cursor> | 299 | <li data-k="go home" data-cursor> | ||
| 269 | <a href="/" tabindex="-1"> | 300 | <a href="/" tabindex="-1"> | ||
| 270 | - | <span class="k">go</span>home</a> | 301 | + | <span class="lbl">home</span> |
| 302 | + | <span class="k">go</span> | |||
| 303 | + | </a> | |||
| 271 | </li> | 304 | </li> | ||
| 272 | <li data-k="section summary"> | 305 | <li data-k="section summary"> | ||
| 273 | <a href="/long" tabindex="-1"> | 306 | <a href="/long" tabindex="-1"> | ||
| 274 | - | <span class="k">section</span>summary</a> | 307 | + | <span class="lbl">summary</span> |
| 308 | + | <span class="k">section</span> | |||
| 309 | + | </a> | |||
| 275 | </li> | 310 | </li> | ||
| 276 | - | <li data-k="section tree"> | 311 | + | <li data-k="section files"> |
| 277 | <a href="/long/tree/main" tabindex="-1"> | 312 | <a href="/long/tree/main" tabindex="-1"> | ||
| 278 | - | <span class="k">section</span>tree</a> | 313 | + | <span class="lbl">files</span> |
| 314 | + | <span class="k">section</span> | |||
| 315 | + | </a> | |||
| 279 | </li> | 316 | </li> | ||
| 280 | <li data-k="section commits"> | 317 | <li data-k="section commits"> | ||
| 281 | <a href="/long/commits/main" tabindex="-1"> | 318 | <a href="/long/commits/main" tabindex="-1"> | ||
| 282 | - | <span class="k">section</span>commits</a> | 319 | + | <span class="lbl">commits</span> |
| 320 | + | <span class="k">section</span> | |||
| 321 | + | </a> | |||
| 283 | </li> | 322 | </li> | ||
| 284 | - | <li data-k="section refs"> | 323 | + | <li data-k="section branches & tags"> |
| 285 | <a href="/long/refs" tabindex="-1"> | 324 | <a href="/long/refs" tabindex="-1"> | ||
| 286 | - | <span class="k">section</span>refs</a> | 325 | + | <span class="lbl">branches & tags</span> |
| 326 | + | <span class="k">section</span> | |||
| 327 | + | </a> | |||
| 287 | </li> | 328 | </li> | ||
| 288 | <li data-k="section merge requests"> | 329 | <li data-k="section merge requests"> | ||
| 289 | <a href="/long/merge-requests" tabindex="-1"> | 330 | <a href="/long/merge-requests" tabindex="-1"> | ||
| 290 | - | <span class="k">section</span>merge requests</a> | 331 | + | <span class="lbl">merge requests</span> |
| 332 | + | <span class="k">section</span> | |||
| 333 | + | </a> | |||
| 291 | </li> | 334 | </li> | ||
| 292 | <li data-k="branch main"> | 335 | <li data-k="branch main"> | ||
| 293 | <a href="/long/tree/main" tabindex="-1"> | 336 | <a href="/long/tree/main" tabindex="-1"> | ||
| 294 | - | <span class="k">branch</span>main</a> | 337 | + | <span class="lbl">main</span> |
| 338 | + | <span class="k">branch</span> | |||
| 339 | + | </a> | |||
| 295 | </li> | 340 | </li> | ||
| 296 | <li data-k="repo diffs"> | 341 | <li data-k="repo diffs"> | ||
| 297 | <a href="/diffs" tabindex="-1"> | 342 | <a href="/diffs" tabindex="-1"> | ||
| 298 | - | <span class="k">repo</span>diffs</a> | 343 | + | <span class="lbl">diffs</span> |
| 344 | + | <span class="k">repo</span> | |||
| 345 | + | </a> | |||
| 299 | </li> | 346 | </li> | ||
| 300 | </ul> | 347 | </ul> | ||
| 348 | + | <p class="panel-foot"> | |||
| 349 | + | <span> | |||
| 350 | + | <kbd>↑</kbd> | |||
| 351 | + | <kbd>↓</kbd> move</span> | |||
| 352 | + | <span> | |||
| 353 | + | <kbd>enter</kbd> open</span> | |||
| 354 | + | <span> | |||
| 355 | + | <kbd>esc</kbd> close</span> | |||
| 356 | + | </p> | |||
| 301 | </dialog> | 357 | </dialog> | ||
| 302 | - | <footer class="bar status" aria-label="Status"> | 358 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 303 | - | <span class="mode">commits</span> | |||
| 304 | - | <span class="st-ctx">main · 2 commits</span> | |||
| 305 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 306 | - | <span class="hi">:</span> go to…</button> | |||
| 307 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 308 | - | </span> | |||
| 309 | - | <span class="st-hints">j/k move · enter open · / filter · ? help</span> | |||
| 310 | - | <span class="st-src"> | |||
| 311 | - | <span class="st-ver">ermite devel · </span> | |||
| 312 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 313 | - | </span> | |||
| 314 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 315 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/2</span> | |||
| 316 | - | </footer> | |||
| 317 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 318 | if (q('dialog[open]').count) return; | 359 | if (q('dialog[open]').count) return; | ||
| 319 | let rows = q('[data-rows] > :not([hidden])').arr(); | 360 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 320 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 361 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 321 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 362 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 322 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 363 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 323 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 364 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 365 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 366 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 324 | })(1); | 367 | })(1); | ||
| 325 | - | k-k from:body -> ((d) => { | 368 | + | k-k from:body -> (async (d) => { |
| 326 | if (q('dialog[open]').count) return; | 369 | if (q('dialog[open]').count) return; | ||
| 327 | let rows = q('[data-rows] > :not([hidden])').arr(); | 370 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 328 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 371 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 329 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 372 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 330 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 373 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 331 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 374 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 375 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 376 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 332 | })(-1); | 377 | })(-1); | ||
| 333 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 378 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 334 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 379 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 335 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 380 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 336 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 381 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 337 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 382 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 338 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 383 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 339 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 384 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 340 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 385 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 341 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 386 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 342 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 387 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 343 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 388 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 344 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 389 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 345 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 390 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 346 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 391 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 347 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 392 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 348 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 393 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 349 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 394 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 350 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 395 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 351 | k-next from:body -> { | 396 | k-next from:body -> { | ||
| 352 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 397 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 353 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 398 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commits-path.html +158 -113
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>history of a.txt · long · example.test</title> | 7 | <title>history of a.txt · long · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,33 +42,52 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">main</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/long" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/long" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/long/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 68 | + | <a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/long/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 72 | + | <a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/long/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work has-rail"> | 86 | <div class="work has-rail"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <div class="filter"> | 88 | <div class="filter"> | ||
| 66 | - | <label for="commit-filter">filter></label> | 89 | + | <label for="commit-filter" class="sr">filter</label> |
| 67 | - | <input id="commit-filter" type="search" data-filter placeholder="subject, sha or author ( / )" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | 90 | + | <input id="commit-filter" type="search" data-filter placeholder="Filter by subject, sha or author…" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); |
| 68 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | 91 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | ||
| 69 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | 92 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | ||
| 70 | </div> | 93 | </div> | ||
| @@ -115,10 +138,8 @@ | |||||
| 115 | </div> | 138 | </div> | ||
| 116 | </main> | 139 | </main> | ||
| 117 | <aside class="rail" aria-label="Context"> | 140 | <aside class="rail" aria-label="Context"> | ||
| 118 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 119 | - | <summary>history · refs on this page</summary> | |||
| 120 | <section aria-labelledby="r-hist"> | 141 | <section aria-labelledby="r-hist"> | ||
| 121 | - | <h2 id="r-hist">history</h2> | 142 | + | <h2 id="r-hist">History</h2> |
| 122 | <dl class="kv"> | 143 | <dl class="kv"> | ||
| 123 | <dt>ref</dt> | 144 | <dt>ref</dt> | ||
| 124 | <dd>main</dd> | 145 | <dd>main</dd> | ||
| @@ -133,76 +154,76 @@ | |||||
| 133 | </dl> | 154 | </dl> | ||
| 134 | </section> | 155 | </section> | ||
| 135 | <section aria-labelledby="r-refs"> | 156 | <section aria-labelledby="r-refs"> | ||
| 136 | - | <h2 id="r-refs">refs on this page</h2> | 157 | + | <h2 id="r-refs">Refs on this page</h2> |
| 137 | <p class="dim">no tag or branch head here</p> | 158 | <p class="dim">no tag or branch head here</p> | ||
| 138 | </section> | 159 | </section> | ||
| 139 | - | </details> | |||
| 140 | </aside> | 160 | </aside> | ||
| 141 | </div> | 161 | </div> | ||
| 142 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 162 | + | <footer class="foot"> |
| 163 | + | <span>ermite devel</span> | |||
| 164 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 165 | + | <span class="foot-keys"> | |||
| 166 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 167 | + | </footer> | |||
| 168 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 169 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 170 | + | </span> | |||
| 171 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 172 | + | </span> | |||
| 173 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 174 | + | </span> | |||
| 175 | + | </div> | |||
| 176 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 143 | <div class="panel-head"> | 177 | <div class="panel-head"> | ||
| 144 | - | <span>keys</span> | 178 | + | <h2>Keyboard shortcuts</h2> |
| 145 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 179 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 180 | + | <kbd>esc</kbd> close</button> | |||
| 146 | </div> | 181 | </div> | ||
| 147 | <dl class="keys"> | 182 | <dl class="keys"> | ||
| 148 | <div> | 183 | <div> | ||
| 149 | <dt> | 184 | <dt> | ||
| 150 | - | <kbd>j</kbd> | 185 | + | <kbd>ctrl k</kbd> |
| 151 | - | <kbd>k</kbd> | 186 | + | <kbd>:</kbd> |
| 152 | </dt> | 187 | </dt> | ||
| 153 | - | <dd>move cursor down / up</dd> | 188 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 154 | </div> | 189 | </div> | ||
| 155 | <div> | 190 | <div> | ||
| 156 | <dt> | 191 | <dt> | ||
| 157 | - | <kbd>enter</kbd> | 192 | + | <kbd>?</kbd> |
| 158 | </dt> | 193 | </dt> | ||
| 159 | - | <dd>open row under cursor</dd> | 194 | + | <dd>this list</dd> |
| 160 | </div> | 195 | </div> | ||
| 161 | <div> | 196 | <div> | ||
| 162 | <dt> | 197 | <dt> | ||
| 163 | - | <kbd>g</kbd> | 198 | + | <kbd>j</kbd> |
| 164 | - | <kbd>g</kbd> / <kbd>G</kbd> | 199 | + | <kbd>k</kbd> |
| 165 | </dt> | 200 | </dt> | ||
| 166 | - | <dd>first / last row</dd> | 201 | + | <dd>move the row cursor down / up</dd> |
| 167 | </div> | 202 | </div> | ||
| 168 | <div> | 203 | <div> | ||
| 169 | <dt> | 204 | <dt> | ||
| 170 | - | <kbd>/</kbd> | 205 | + | <kbd>enter</kbd> |
| 171 | </dt> | 206 | </dt> | ||
| 172 | - | <dd>focus the filter</dd> | 207 | + | <dd>open the row under the cursor</dd> |
| 173 | </div> | 208 | </div> | ||
| 174 | <div> | 209 | <div> | ||
| 175 | <dt> | 210 | <dt> | ||
| 176 | - | <kbd>g h</kbd> | 211 | + | <kbd>g g</kbd> |
| 177 | - | <kbd>g t</kbd> | 212 | + | <kbd>G</kbd> |
| 178 | </dt> | 213 | </dt> | ||
| 179 | - | <dd>go home / tree</dd> | 214 | + | <dd>first / last row</dd> |
| 180 | </div> | 215 | </div> | ||
| 181 | <div> | 216 | <div> | ||
| 182 | <dt> | 217 | <dt> | ||
| 183 | - | <kbd>g c</kbd> | 218 | + | <kbd>/</kbd> |
| 184 | - | <kbd>g r</kbd> | |||
| 185 | </dt> | 219 | </dt> | ||
| 186 | - | <dd>go commits / refs</dd> | 220 | + | <dd>filter the list</dd> |
| 187 | </div> | 221 | </div> | ||
| 188 | <div> | 222 | <div> | ||
| 189 | <dt> | 223 | <dt> | ||
| 190 | <kbd>b</kbd> | 224 | <kbd>b</kbd> | ||
| 191 | </dt> | 225 | </dt> | ||
| 192 | - | <dd>switch ref (branches, tags)</dd> | 226 | + | <dd>switch branch or tag</dd> |
| 193 | - | </div> | |||
| 194 | - | <div> | |||
| 195 | - | <dt> | |||
| 196 | - | <kbd>:</kbd> | |||
| 197 | - | <kbd>f</kbd> | |||
| 198 | - | </dt> | |||
| 199 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 200 | - | </div> | |||
| 201 | - | <div> | |||
| 202 | - | <dt> | |||
| 203 | - | <kbd>y</kbd> | |||
| 204 | - | </dt> | |||
| 205 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 206 | </div> | 227 | </div> | ||
| 207 | <div> | 228 | <div> | ||
| 208 | <dt> | 229 | <dt> | ||
| @@ -210,15 +231,21 @@ | |||||
| 210 | <kbd>t</kbd> | 231 | <kbd>t</kbd> | ||
| 211 | <kbd>c</kbd> | 232 | <kbd>c</kbd> | ||
| 212 | <kbd>r</kbd> | 233 | <kbd>r</kbd> | ||
| 234 | + | <kbd>m</kbd> | |||
| 213 | </dt> | 235 | </dt> | ||
| 214 | - | <dd>repo sections (when shown)</dd> | 236 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 215 | </div> | 237 | </div> | ||
| 216 | <div> | 238 | <div> | ||
| 217 | <dt> | 239 | <dt> | ||
| 218 | - | <kbd>m</kbd> | 240 | + | <kbd>g h</kbd> |
| 219 | - | <kbd>g m</kbd> | |||
| 220 | </dt> | 241 | </dt> | ||
| 221 | - | <dd>merge requests (when shown)</dd> | 242 | + | <dd>home</dd> |
| 243 | + | </div> | |||
| 244 | + | <div> | |||
| 245 | + | <dt> | |||
| 246 | + | <kbd>y</kbd> | |||
| 247 | + | </dt> | |||
| 248 | + | <dd>copy a permalink pinned to the commit</dd> | |||
| 222 | </div> | 249 | </div> | ||
| 223 | <div> | 250 | <div> | ||
| 224 | <dt> | 251 | <dt> | ||
| @@ -238,19 +265,21 @@ | |||||
| 238 | <dt> | 265 | <dt> | ||
| 239 | <kbd>esc</kbd> | 266 | <kbd>esc</kbd> | ||
| 240 | </dt> | 267 | </dt> | ||
| 241 | - | <dd>close panel</dd> | 268 | + | <dd>close a dialog, leave a field</dd> |
| 242 | </div> | 269 | </div> | ||
| 243 | </dl> | 270 | </dl> | ||
| 244 | </dialog> | 271 | </dialog> | ||
| 245 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 272 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 246 | <div class="panel-head"> | 273 | <div class="panel-head"> | ||
| 247 | - | <label for="refs-filter">ref></label> | 274 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 248 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 275 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 249 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 276 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 277 | + | <kbd>esc</kbd> | |||
| 278 | + | </button> | |||
| 250 | </div> | 279 | </div> | ||
| 251 | <div class="panel-cols" data-picklist> | 280 | <div class="panel-cols" data-picklist> | ||
| 252 | <section aria-label="Branches"> | 281 | <section aria-label="Branches"> | ||
| 253 | - | <h2>branches <span class="mute">1</span> | 282 | + | <h2>Branches <span class="count">1</span> |
| 254 | </h2> | 283 | </h2> | ||
| 255 | <ul> | 284 | <ul> | ||
| 256 | <li> | 285 | <li> | ||
| @@ -260,109 +289,125 @@ | |||||
| 260 | </ul> | 289 | </ul> | ||
| 261 | </section> | 290 | </section> | ||
| 262 | <section aria-label="Tags"> | 291 | <section aria-label="Tags"> | ||
| 263 | - | <h2>tags <span class="mute">0</span> | 292 | + | <h2>Tags <span class="count">0</span> |
| 264 | </h2> | 293 | </h2> | ||
| 265 | <ul> | 294 | <ul> | ||
| 266 | </ul> | 295 | </ul> | ||
| 267 | </section> | 296 | </section> | ||
| 268 | </div> | 297 | </div> | ||
| 269 | </dialog> | 298 | </dialog> | ||
| 270 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 299 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 271 | <div class="panel-head"> | 300 | <div class="panel-head"> | ||
| 272 | - | <label for="cmd-q" class="hi">:</label> | 301 | + | <label for="cmd-q" class="sr">go to</label> |
| 273 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 302 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 274 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 303 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 275 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 304 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 276 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 305 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 277 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 306 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 278 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 307 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 279 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 308 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 280 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 309 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 310 | + | <kbd>esc</kbd> | |||
| 311 | + | </button> | |||
| 281 | </div> | 312 | </div> | ||
| 282 | - | <ul id="cmd-list" class="cmd-list"> | 313 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 283 | <li data-k="go home" data-cursor> | 314 | <li data-k="go home" data-cursor> | ||
| 284 | <a href="/" tabindex="-1"> | 315 | <a href="/" tabindex="-1"> | ||
| 285 | - | <span class="k">go</span>home</a> | 316 | + | <span class="lbl">home</span> |
| 317 | + | <span class="k">go</span> | |||
| 318 | + | </a> | |||
| 286 | </li> | 319 | </li> | ||
| 287 | <li data-k="section summary"> | 320 | <li data-k="section summary"> | ||
| 288 | <a href="/long" tabindex="-1"> | 321 | <a href="/long" tabindex="-1"> | ||
| 289 | - | <span class="k">section</span>summary</a> | 322 | + | <span class="lbl">summary</span> |
| 323 | + | <span class="k">section</span> | |||
| 324 | + | </a> | |||
| 290 | </li> | 325 | </li> | ||
| 291 | - | <li data-k="section tree"> | 326 | + | <li data-k="section files"> |
| 292 | <a href="/long/tree/main" tabindex="-1"> | 327 | <a href="/long/tree/main" tabindex="-1"> | ||
| 293 | - | <span class="k">section</span>tree</a> | 328 | + | <span class="lbl">files</span> |
| 329 | + | <span class="k">section</span> | |||
| 330 | + | </a> | |||
| 294 | </li> | 331 | </li> | ||
| 295 | <li data-k="section commits"> | 332 | <li data-k="section commits"> | ||
| 296 | <a href="/long/commits/main" tabindex="-1"> | 333 | <a href="/long/commits/main" tabindex="-1"> | ||
| 297 | - | <span class="k">section</span>commits</a> | 334 | + | <span class="lbl">commits</span> |
| 335 | + | <span class="k">section</span> | |||
| 336 | + | </a> | |||
| 298 | </li> | 337 | </li> | ||
| 299 | - | <li data-k="section refs"> | 338 | + | <li data-k="section branches & tags"> |
| 300 | <a href="/long/refs" tabindex="-1"> | 339 | <a href="/long/refs" tabindex="-1"> | ||
| 301 | - | <span class="k">section</span>refs</a> | 340 | + | <span class="lbl">branches & tags</span> |
| 341 | + | <span class="k">section</span> | |||
| 342 | + | </a> | |||
| 302 | </li> | 343 | </li> | ||
| 303 | <li data-k="section merge requests"> | 344 | <li data-k="section merge requests"> | ||
| 304 | <a href="/long/merge-requests" tabindex="-1"> | 345 | <a href="/long/merge-requests" tabindex="-1"> | ||
| 305 | - | <span class="k">section</span>merge requests</a> | 346 | + | <span class="lbl">merge requests</span> |
| 347 | + | <span class="k">section</span> | |||
| 348 | + | </a> | |||
| 306 | </li> | 349 | </li> | ||
| 307 | <li data-k="branch main"> | 350 | <li data-k="branch main"> | ||
| 308 | <a href="/long/tree/main" tabindex="-1"> | 351 | <a href="/long/tree/main" tabindex="-1"> | ||
| 309 | - | <span class="k">branch</span>main</a> | 352 | + | <span class="lbl">main</span> |
| 353 | + | <span class="k">branch</span> | |||
| 354 | + | </a> | |||
| 310 | </li> | 355 | </li> | ||
| 311 | <li data-k="repo diffs"> | 356 | <li data-k="repo diffs"> | ||
| 312 | <a href="/diffs" tabindex="-1"> | 357 | <a href="/diffs" tabindex="-1"> | ||
| 313 | - | <span class="k">repo</span>diffs</a> | 358 | + | <span class="lbl">diffs</span> |
| 359 | + | <span class="k">repo</span> | |||
| 360 | + | </a> | |||
| 314 | </li> | 361 | </li> | ||
| 315 | </ul> | 362 | </ul> | ||
| 363 | + | <p class="panel-foot"> | |||
| 364 | + | <span> | |||
| 365 | + | <kbd>↑</kbd> | |||
| 366 | + | <kbd>↓</kbd> move</span> | |||
| 367 | + | <span> | |||
| 368 | + | <kbd>enter</kbd> open</span> | |||
| 369 | + | <span> | |||
| 370 | + | <kbd>esc</kbd> close</span> | |||
| 371 | + | </p> | |||
| 316 | </dialog> | 372 | </dialog> | ||
| 317 | - | <footer class="bar status" aria-label="Status"> | 373 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 318 | - | <span class="mode">commits</span> | |||
| 319 | - | <span class="st-ctx">main · history of a.txt · 2 commits</span> | |||
| 320 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 321 | - | <span class="hi">:</span> go to…</button> | |||
| 322 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 323 | - | </span> | |||
| 324 | - | <span class="st-hints">j/k move · enter open · / filter · ? help</span> | |||
| 325 | - | <span class="st-src"> | |||
| 326 | - | <span class="st-ver">ermite devel · </span> | |||
| 327 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 328 | - | </span> | |||
| 329 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 330 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/2</span> | |||
| 331 | - | </footer> | |||
| 332 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 333 | if (q('dialog[open]').count) return; | 374 | if (q('dialog[open]').count) return; | ||
| 334 | let rows = q('[data-rows] > :not([hidden])').arr(); | 375 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 335 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 376 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 336 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 377 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 337 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 378 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 338 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 379 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 380 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 381 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 339 | })(1); | 382 | })(1); | ||
| 340 | - | k-k from:body -> ((d) => { | 383 | + | k-k from:body -> (async (d) => { |
| 341 | if (q('dialog[open]').count) return; | 384 | if (q('dialog[open]').count) return; | ||
| 342 | let rows = q('[data-rows] > :not([hidden])').arr(); | 385 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 343 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 386 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 344 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 387 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 345 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 388 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 346 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 389 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 390 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 391 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 347 | })(-1); | 392 | })(-1); | ||
| 348 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 393 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 349 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 394 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 350 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 395 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 351 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 396 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 352 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 397 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 353 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 398 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 354 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 399 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 355 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 400 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 356 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 401 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 357 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 402 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 358 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 403 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 359 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 404 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 360 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 405 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 361 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 406 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 362 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 407 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 363 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 408 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 364 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 409 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 365 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 410 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 366 | k-next from:body -> { | 411 | k-next from:body -> { | ||
| 367 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 412 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 368 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 413 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commits-root.html +166 -119
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>commits · long · example.test</title> | 7 | <title>commits · long · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,33 +39,52 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">main</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/long" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/long" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/long/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/long/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/long/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <div class="filter"> | 85 | <div class="filter"> | ||
| 63 | - | <label for="commit-filter">filter></label> | 86 | + | <label for="commit-filter" class="sr">filter</label> |
| 64 | - | <input id="commit-filter" type="search" data-filter placeholder="subject, sha or author ( / )" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | 87 | + | <input id="commit-filter" type="search" data-filter placeholder="Filter by subject, sha or author…" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); |
| 65 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | 88 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | ||
| 66 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | 89 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | ||
| 67 | </div> | 90 | </div> | ||
| @@ -80,7 +103,7 @@ | |||||
| 80 | <td class="c-sha mute">SHA7</td> | 103 | <td class="c-sha mute">SHA7</td> | ||
| 81 | <td class="c-subject"> | 104 | <td class="c-subject"> | ||
| 82 | <a href="/long/commit/SHA40">commit number 51</a> | 105 | <a href="/long/commit/SHA40">commit number 51</a> | ||
| 83 | - | <span class="tagged">[main]</span> | 106 | + | <span class="pill">main</span> |
| 84 | </td> | 107 | </td> | ||
| 85 | <td class="c-who dim">T</td> | 108 | <td class="c-who dim">T</td> | ||
| 86 | <td class="c-when nowrap dim"> | 109 | <td class="c-when nowrap dim"> | ||
| @@ -896,15 +919,13 @@ | |||||
| 896 | </section> | 919 | </section> | ||
| 897 | </div> | 920 | </div> | ||
| 898 | <nav class="pager" aria-label="Pages"> | 921 | <nav class="pager" aria-label="Pages"> | ||
| 899 | - | <span class="mute">← newest</span> | 922 | + | <span class="btn" aria-disabled="true">← Newest</span> |
| 900 | - | <a href="/long/commits/main?page=YzE6MzMzZGFjMWY5YmY4MjIzMmQxOTAzNGVlZTliZmI2NzFmM2QxMWEzNDo1MDplM2IwYzQ0Mjk4ZmM" rel="next">older →</a> | 923 | + | <a class="btn" href="/long/commits/main?page=YzE6MzMzZGFjMWY5YmY4MjIzMmQxOTAzNGVlZTliZmI2NzFmM2QxMWEzNDo1MDplM2IwYzQ0Mjk4ZmM" rel="next">Older →</a> |
| 901 | </nav> | 924 | </nav> | ||
| 902 | </main> | 925 | </main> | ||
| 903 | <aside class="rail" aria-label="Context"> | 926 | <aside class="rail" aria-label="Context"> | ||
| 904 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 905 | - | <summary>history · refs on this page</summary> | |||
| 906 | <section aria-labelledby="r-hist"> | 927 | <section aria-labelledby="r-hist"> | ||
| 907 | - | <h2 id="r-hist">history</h2> | 928 | + | <h2 id="r-hist">History</h2> |
| 908 | <dl class="kv"> | 929 | <dl class="kv"> | ||
| 909 | <dt>ref</dt> | 930 | <dt>ref</dt> | ||
| 910 | <dd>main</dd> | 931 | <dd>main</dd> | ||
| @@ -913,78 +934,83 @@ | |||||
| 913 | </dl> | 934 | </dl> | ||
| 914 | </section> | 935 | </section> | ||
| 915 | <section aria-labelledby="r-refs"> | 936 | <section aria-labelledby="r-refs"> | ||
| 916 | - | <h2 id="r-refs">refs on this page</h2> | 937 | + | <h2 id="r-refs">Refs on this page</h2> |
| 917 | <nav class="outline" aria-label="Refs on this page"> | 938 | <nav class="outline" aria-label="Refs on this page"> | ||
| 918 | <a href="#c-SHA7">branch main</a> | 939 | <a href="#c-SHA7">branch main</a> | ||
| 919 | </nav> | 940 | </nav> | ||
| 920 | </section> | 941 | </section> | ||
| 921 | - | </details> | |||
| 922 | </aside> | 942 | </aside> | ||
| 923 | </div> | 943 | </div> | ||
| 924 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 944 | + | <footer class="foot"> |
| 945 | + | <span>ermite devel</span> | |||
| 946 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 947 | + | <span class="foot-keys"> | |||
| 948 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 949 | + | </footer> | |||
| 950 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 951 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 952 | + | </span> | |||
| 953 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 954 | + | </span> | |||
| 955 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 956 | + | </span> | |||
| 957 | + | <span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Along&view=commits&ref=main&head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |||
| 958 | + | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |||
| 959 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |||
| 960 | + | gap -> ermiteLive.reload()"> | |||
| 961 | + | </span> | |||
| 962 | + | </div> | |||
| 963 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 925 | <div class="panel-head"> | 964 | <div class="panel-head"> | ||
| 926 | - | <span>keys</span> | 965 | + | <h2>Keyboard shortcuts</h2> |
| 927 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 966 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 967 | + | <kbd>esc</kbd> close</button> | |||
| 928 | </div> | 968 | </div> | ||
| 929 | <dl class="keys"> | 969 | <dl class="keys"> | ||
| 930 | <div> | 970 | <div> | ||
| 931 | <dt> | 971 | <dt> | ||
| 932 | - | <kbd>j</kbd> | 972 | + | <kbd>ctrl k</kbd> |
| 933 | - | <kbd>k</kbd> | 973 | + | <kbd>:</kbd> |
| 934 | </dt> | 974 | </dt> | ||
| 935 | - | <dd>move cursor down / up</dd> | 975 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 936 | </div> | 976 | </div> | ||
| 937 | <div> | 977 | <div> | ||
| 938 | <dt> | 978 | <dt> | ||
| 939 | - | <kbd>enter</kbd> | 979 | + | <kbd>?</kbd> |
| 940 | </dt> | 980 | </dt> | ||
| 941 | - | <dd>open row under cursor</dd> | 981 | + | <dd>this list</dd> |
| 942 | </div> | 982 | </div> | ||
| 943 | <div> | 983 | <div> | ||
| 944 | <dt> | 984 | <dt> | ||
| 945 | - | <kbd>g</kbd> | 985 | + | <kbd>j</kbd> |
| 946 | - | <kbd>g</kbd> / <kbd>G</kbd> | 986 | + | <kbd>k</kbd> |
| 947 | </dt> | 987 | </dt> | ||
| 948 | - | <dd>first / last row</dd> | 988 | + | <dd>move the row cursor down / up</dd> |
| 949 | </div> | 989 | </div> | ||
| 950 | <div> | 990 | <div> | ||
| 951 | <dt> | 991 | <dt> | ||
| 952 | - | <kbd>/</kbd> | 992 | + | <kbd>enter</kbd> |
| 953 | </dt> | 993 | </dt> | ||
| 954 | - | <dd>focus the filter</dd> | 994 | + | <dd>open the row under the cursor</dd> |
| 955 | </div> | 995 | </div> | ||
| 956 | <div> | 996 | <div> | ||
| 957 | <dt> | 997 | <dt> | ||
| 958 | - | <kbd>g h</kbd> | 998 | + | <kbd>g g</kbd> |
| 959 | - | <kbd>g t</kbd> | 999 | + | <kbd>G</kbd> |
| 960 | </dt> | 1000 | </dt> | ||
| 961 | - | <dd>go home / tree</dd> | 1001 | + | <dd>first / last row</dd> |
| 962 | </div> | 1002 | </div> | ||
| 963 | <div> | 1003 | <div> | ||
| 964 | <dt> | 1004 | <dt> | ||
| 965 | - | <kbd>g c</kbd> | 1005 | + | <kbd>/</kbd> |
| 966 | - | <kbd>g r</kbd> | |||
| 967 | </dt> | 1006 | </dt> | ||
| 968 | - | <dd>go commits / refs</dd> | 1007 | + | <dd>filter the list</dd> |
| 969 | </div> | 1008 | </div> | ||
| 970 | <div> | 1009 | <div> | ||
| 971 | <dt> | 1010 | <dt> | ||
| 972 | <kbd>b</kbd> | 1011 | <kbd>b</kbd> | ||
| 973 | </dt> | 1012 | </dt> | ||
| 974 | - | <dd>switch ref (branches, tags)</dd> | 1013 | + | <dd>switch branch or tag</dd> |
| 975 | - | </div> | |||
| 976 | - | <div> | |||
| 977 | - | <dt> | |||
| 978 | - | <kbd>:</kbd> | |||
| 979 | - | <kbd>f</kbd> | |||
| 980 | - | </dt> | |||
| 981 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 982 | - | </div> | |||
| 983 | - | <div> | |||
| 984 | - | <dt> | |||
| 985 | - | <kbd>y</kbd> | |||
| 986 | - | </dt> | |||
| 987 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 988 | </div> | 1014 | </div> | ||
| 989 | <div> | 1015 | <div> | ||
| 990 | <dt> | 1016 | <dt> | ||
| @@ -992,15 +1018,21 @@ | |||||
| 992 | <kbd>t</kbd> | 1018 | <kbd>t</kbd> | ||
| 993 | <kbd>c</kbd> | 1019 | <kbd>c</kbd> | ||
| 994 | <kbd>r</kbd> | 1020 | <kbd>r</kbd> | ||
| 1021 | + | <kbd>m</kbd> | |||
| 995 | </dt> | 1022 | </dt> | ||
| 996 | - | <dd>repo sections (when shown)</dd> | 1023 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 997 | </div> | 1024 | </div> | ||
| 998 | <div> | 1025 | <div> | ||
| 999 | <dt> | 1026 | <dt> | ||
| 1000 | - | <kbd>m</kbd> | 1027 | + | <kbd>g h</kbd> |
| 1001 | - | <kbd>g m</kbd> | 1028 | + | </dt> |
| 1029 | + | <dd>home</dd> | |||
| 1030 | + | </div> | |||
| 1031 | + | <div> | |||
| 1032 | + | <dt> | |||
| 1033 | + | <kbd>y</kbd> | |||
| 1002 | </dt> | 1034 | </dt> | ||
| 1003 | - | <dd>merge requests (when shown)</dd> | 1035 | + | <dd>copy a permalink pinned to the commit</dd> |
| 1004 | </div> | 1036 | </div> | ||
| 1005 | <div> | 1037 | <div> | ||
| 1006 | <dt> | 1038 | <dt> | ||
| @@ -1020,19 +1052,21 @@ | |||||
| 1020 | <dt> | 1052 | <dt> | ||
| 1021 | <kbd>esc</kbd> | 1053 | <kbd>esc</kbd> | ||
| 1022 | </dt> | 1054 | </dt> | ||
| 1023 | - | <dd>close panel</dd> | 1055 | + | <dd>close a dialog, leave a field</dd> |
| 1024 | </div> | 1056 | </div> | ||
| 1025 | </dl> | 1057 | </dl> | ||
| 1026 | </dialog> | 1058 | </dialog> | ||
| 1027 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 1059 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 1028 | <div class="panel-head"> | 1060 | <div class="panel-head"> | ||
| 1029 | - | <label for="refs-filter">ref></label> | 1061 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 1030 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 1062 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 1031 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 1063 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 1064 | + | <kbd>esc</kbd> | |||
| 1065 | + | </button> | |||
| 1032 | </div> | 1066 | </div> | ||
| 1033 | <div class="panel-cols" data-picklist> | 1067 | <div class="panel-cols" data-picklist> | ||
| 1034 | <section aria-label="Branches"> | 1068 | <section aria-label="Branches"> | ||
| 1035 | - | <h2>branches <span class="mute">1</span> | 1069 | + | <h2>Branches <span class="count">1</span> |
| 1036 | </h2> | 1070 | </h2> | ||
| 1037 | <ul> | 1071 | <ul> | ||
| 1038 | <li> | 1072 | <li> | ||
| @@ -1042,112 +1076,125 @@ | |||||
| 1042 | </ul> | 1076 | </ul> | ||
| 1043 | </section> | 1077 | </section> | ||
| 1044 | <section aria-label="Tags"> | 1078 | <section aria-label="Tags"> | ||
| 1045 | - | <h2>tags <span class="mute">0</span> | 1079 | + | <h2>Tags <span class="count">0</span> |
| 1046 | </h2> | 1080 | </h2> | ||
| 1047 | <ul> | 1081 | <ul> | ||
| 1048 | </ul> | 1082 | </ul> | ||
| 1049 | </section> | 1083 | </section> | ||
| 1050 | </div> | 1084 | </div> | ||
| 1051 | </dialog> | 1085 | </dialog> | ||
| 1052 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 1086 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 1053 | <div class="panel-head"> | 1087 | <div class="panel-head"> | ||
| 1054 | - | <label for="cmd-q" class="hi">:</label> | 1088 | + | <label for="cmd-q" class="sr">go to</label> |
| 1055 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 1089 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 1056 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 1090 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 1057 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 1091 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 1058 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 1092 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 1059 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 1093 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 1060 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 1094 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 1061 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 1095 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 1062 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 1096 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 1097 | + | <kbd>esc</kbd> | |||
| 1098 | + | </button> | |||
| 1063 | </div> | 1099 | </div> | ||
| 1064 | - | <ul id="cmd-list" class="cmd-list"> | 1100 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 1065 | <li data-k="go home" data-cursor> | 1101 | <li data-k="go home" data-cursor> | ||
| 1066 | <a href="/" tabindex="-1"> | 1102 | <a href="/" tabindex="-1"> | ||
| 1067 | - | <span class="k">go</span>home</a> | 1103 | + | <span class="lbl">home</span> |
| 1104 | + | <span class="k">go</span> | |||
| 1105 | + | </a> | |||
| 1068 | </li> | 1106 | </li> | ||
| 1069 | <li data-k="section summary"> | 1107 | <li data-k="section summary"> | ||
| 1070 | <a href="/long" tabindex="-1"> | 1108 | <a href="/long" tabindex="-1"> | ||
| 1071 | - | <span class="k">section</span>summary</a> | 1109 | + | <span class="lbl">summary</span> |
| 1110 | + | <span class="k">section</span> | |||
| 1111 | + | </a> | |||
| 1072 | </li> | 1112 | </li> | ||
| 1073 | - | <li data-k="section tree"> | 1113 | + | <li data-k="section files"> |
| 1074 | <a href="/long/tree/main" tabindex="-1"> | 1114 | <a href="/long/tree/main" tabindex="-1"> | ||
| 1075 | - | <span class="k">section</span>tree</a> | 1115 | + | <span class="lbl">files</span> |
| 1116 | + | <span class="k">section</span> | |||
| 1117 | + | </a> | |||
| 1076 | </li> | 1118 | </li> | ||
| 1077 | <li data-k="section commits"> | 1119 | <li data-k="section commits"> | ||
| 1078 | <a href="/long/commits/main" tabindex="-1"> | 1120 | <a href="/long/commits/main" tabindex="-1"> | ||
| 1079 | - | <span class="k">section</span>commits</a> | 1121 | + | <span class="lbl">commits</span> |
| 1122 | + | <span class="k">section</span> | |||
| 1123 | + | </a> | |||
| 1080 | </li> | 1124 | </li> | ||
| 1081 | - | <li data-k="section refs"> | 1125 | + | <li data-k="section branches & tags"> |
| 1082 | <a href="/long/refs" tabindex="-1"> | 1126 | <a href="/long/refs" tabindex="-1"> | ||
| 1083 | - | <span class="k">section</span>refs</a> | 1127 | + | <span class="lbl">branches & tags</span> |
| 1128 | + | <span class="k">section</span> | |||
| 1129 | + | </a> | |||
| 1084 | </li> | 1130 | </li> | ||
| 1085 | <li data-k="section merge requests"> | 1131 | <li data-k="section merge requests"> | ||
| 1086 | <a href="/long/merge-requests" tabindex="-1"> | 1132 | <a href="/long/merge-requests" tabindex="-1"> | ||
| 1087 | - | <span class="k">section</span>merge requests</a> | 1133 | + | <span class="lbl">merge requests</span> |
| 1134 | + | <span class="k">section</span> | |||
| 1135 | + | </a> | |||
| 1088 | </li> | 1136 | </li> | ||
| 1089 | <li data-k="branch main"> | 1137 | <li data-k="branch main"> | ||
| 1090 | <a href="/long/tree/main" tabindex="-1"> | 1138 | <a href="/long/tree/main" tabindex="-1"> | ||
| 1091 | - | <span class="k">branch</span>main</a> | 1139 | + | <span class="lbl">main</span> |
| 1140 | + | <span class="k">branch</span> | |||
| 1141 | + | </a> | |||
| 1092 | </li> | 1142 | </li> | ||
| 1093 | <li data-k="repo diffs"> | 1143 | <li data-k="repo diffs"> | ||
| 1094 | <a href="/diffs" tabindex="-1"> | 1144 | <a href="/diffs" tabindex="-1"> | ||
| 1095 | - | <span class="k">repo</span>diffs</a> | 1145 | + | <span class="lbl">diffs</span> |
| 1146 | + | <span class="k">repo</span> | |||
| 1147 | + | </a> | |||
| 1096 | </li> | 1148 | </li> | ||
| 1097 | </ul> | 1149 | </ul> | ||
| 1150 | + | <p class="panel-foot"> | |||
| 1151 | + | <span> | |||
| 1152 | + | <kbd>↑</kbd> | |||
| 1153 | + | <kbd>↓</kbd> move</span> | |||
| 1154 | + | <span> | |||
| 1155 | + | <kbd>enter</kbd> open</span> | |||
| 1156 | + | <span> | |||
| 1157 | + | <kbd>esc</kbd> close</span> | |||
| 1158 | + | </p> | |||
| 1098 | </dialog> | 1159 | </dialog> | ||
| 1099 | - | <footer class="bar status" aria-label="Status"> | 1160 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 1100 | - | <span class="mode">commits</span> | |||
| 1101 | - | <span class="st-ctx">main · 50 commits · more</span> | |||
| 1102 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 1103 | - | <span class="hi">:</span> go to…</button> | |||
| 1104 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 1105 | - | </span> | |||
| 1106 | - | <span class="st-hints">j/k move · enter open · / filter · ? help</span> | |||
| 1107 | - | <span class="st-src"> | |||
| 1108 | - | <span class="st-ver">ermite devel · </span> | |||
| 1109 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 1110 | - | </span> | |||
| 1111 | - | <span class="st-live" data-state="offline" role="status" hx-sse:connect="/-/events?topic=repo%3Along&view=commits&ref=main&head=SHA40" hx-swap="none" :text="data.state" :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |||
| 1112 | - | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |||
| 1113 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |||
| 1114 | - | gap -> ermiteLive.reload()">offline</span> | |||
| 1115 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/50</span> | |||
| 1116 | - | </footer> | |||
| 1117 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 1118 | if (q('dialog[open]').count) return; | 1161 | if (q('dialog[open]').count) return; | ||
| 1119 | let rows = q('[data-rows] > :not([hidden])').arr(); | 1162 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 1120 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 1163 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 1121 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 1164 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 1122 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 1165 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 1123 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 1166 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 1167 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 1168 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 1124 | })(1); | 1169 | })(1); | ||
| 1125 | - | k-k from:body -> ((d) => { | 1170 | + | k-k from:body -> (async (d) => { |
| 1126 | if (q('dialog[open]').count) return; | 1171 | if (q('dialog[open]').count) return; | ||
| 1127 | let rows = q('[data-rows] > :not([hidden])').arr(); | 1172 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 1128 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 1173 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 1129 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 1174 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 1130 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 1175 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 1131 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 1176 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 1177 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 1178 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 1132 | })(-1); | 1179 | })(-1); | ||
| 1133 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 1180 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 1134 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 1181 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 1135 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 1182 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 1136 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 1183 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 1137 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 1184 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 1138 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 1185 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 1139 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 1186 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 1140 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 1187 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 1141 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 1188 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 1142 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 1189 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 1143 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 1190 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 1144 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 1191 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 1145 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 1192 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 1146 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 1193 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 1147 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 1194 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 1148 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 1195 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 1149 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 1196 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 1150 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 1197 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 1151 | k-next from:body -> { | 1198 | k-next from:body -> { | ||
| 1152 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 1199 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 1153 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 1200 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/commits-small.html +174 -122
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>commits · diffs · example.test</title> | 7 | <title>commits · diffs · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,33 +39,52 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">main</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/diffs" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 65 | + | <a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="g c" aria-current="page">commits</a> | 69 | + | <a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work has-rail"> | 83 | <div class="work has-rail"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <div class="filter"> | 85 | <div class="filter"> | ||
| 63 | - | <label for="commit-filter">filter></label> | 86 | + | <label for="commit-filter" class="sr">filter</label> |
| 64 | - | <input id="commit-filter" type="search" data-filter placeholder="subject, sha or author ( / )" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | 87 | + | <input id="commit-filter" type="search" data-filter placeholder="Filter by subject, sha or author…" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); |
| 65 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | 88 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | ||
| 66 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | 89 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | ||
| 67 | </div> | 90 | </div> | ||
| @@ -80,7 +103,7 @@ | |||||
| 80 | <td class="c-sha mute">SHA7</td> | 103 | <td class="c-sha mute">SHA7</td> | ||
| 81 | <td class="c-subject"> | 104 | <td class="c-subject"> | ||
| 82 | <a href="/diffs/commit/SHA40">add a very large file</a> | 105 | <a href="/diffs/commit/SHA40">add a very large file</a> | ||
| 83 | - | <span class="tagged">[main]</span> | 106 | + | <span class="pill">main</span> |
| 84 | </td> | 107 | </td> | ||
| 85 | <td class="c-who dim">T</td> | 108 | <td class="c-who dim">T</td> | ||
| 86 | <td class="c-when nowrap dim"> | 109 | <td class="c-when nowrap dim"> | ||
| @@ -123,7 +146,7 @@ | |||||
| 123 | <td class="c-sha mute">SHA7</td> | 146 | <td class="c-sha mute">SHA7</td> | ||
| 124 | <td class="c-subject"> | 147 | <td class="c-subject"> | ||
| 125 | <a href="/diffs/commit/SHA40">merge the side branch</a> | 148 | <a href="/diffs/commit/SHA40">merge the side branch</a> | ||
| 126 | - | <span class="tagged">(merge)</span> | 149 | + | <span class="pill">merge</span> |
| 127 | </td> | 150 | </td> | ||
| 128 | <td class="c-who dim">T</td> | 151 | <td class="c-who dim">T</td> | ||
| 129 | <td class="c-when nowrap dim"> | 152 | <td class="c-when nowrap dim"> | ||
| @@ -145,7 +168,7 @@ | |||||
| 145 | <td class="c-sha mute">SHA7</td> | 168 | <td class="c-sha mute">SHA7</td> | ||
| 146 | <td class="c-subject"> | 169 | <td class="c-subject"> | ||
| 147 | <a href="/diffs/commit/SHA40">work on the side</a> | 170 | <a href="/diffs/commit/SHA40">work on the side</a> | ||
| 148 | - | <span class="tagged">[side]</span> | 171 | + | <span class="pill">side</span> |
| 149 | </td> | 172 | </td> | ||
| 150 | <td class="c-who dim">T</td> | 173 | <td class="c-who dim">T</td> | ||
| 151 | <td class="c-when nowrap dim"> | 174 | <td class="c-when nowrap dim"> | ||
| @@ -209,7 +232,8 @@ | |||||
| 209 | <td class="c-sha mute">SHA7</td> | 232 | <td class="c-sha mute">SHA7</td> | ||
| 210 | <td class="c-subject"> | 233 | <td class="c-subject"> | ||
| 211 | <a href="/diffs/commit/SHA40">modify a file and make a script executable</a> | 234 | <a href="/diffs/commit/SHA40">modify a file and make a script executable</a> | ||
| 212 | - | <span class="tagged">[tag: v1.0.0]</span> | 235 | + | <span class="pill tag"> |
| 236 | + | <span class="sr">tag </span>v1.0.0</span> | |||
| 213 | </td> | 237 | </td> | ||
| 214 | <td class="c-who dim">T</td> | 238 | <td class="c-who dim">T</td> | ||
| 215 | <td class="c-when nowrap dim"> | 239 | <td class="c-when nowrap dim"> | ||
| @@ -243,10 +267,8 @@ | |||||
| 243 | </div> | 267 | </div> | ||
| 244 | </main> | 268 | </main> | ||
| 245 | <aside class="rail" aria-label="Context"> | 269 | <aside class="rail" aria-label="Context"> | ||
| 246 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 247 | - | <summary>history · refs on this page</summary> | |||
| 248 | <section aria-labelledby="r-hist"> | 270 | <section aria-labelledby="r-hist"> | ||
| 249 | - | <h2 id="r-hist">history</h2> | 271 | + | <h2 id="r-hist">History</h2> |
| 250 | <dl class="kv"> | 272 | <dl class="kv"> | ||
| 251 | <dt>ref</dt> | 273 | <dt>ref</dt> | ||
| 252 | <dd>main</dd> | 274 | <dd>main</dd> | ||
| @@ -255,80 +277,85 @@ | |||||
| 255 | </dl> | 277 | </dl> | ||
| 256 | </section> | 278 | </section> | ||
| 257 | <section aria-labelledby="r-refs"> | 279 | <section aria-labelledby="r-refs"> | ||
| 258 | - | <h2 id="r-refs">refs on this page</h2> | 280 | + | <h2 id="r-refs">Refs on this page</h2> |
| 259 | <nav class="outline" aria-label="Refs on this page"> | 281 | <nav class="outline" aria-label="Refs on this page"> | ||
| 260 | <a href="#c-SHA7">branch main</a> | 282 | <a href="#c-SHA7">branch main</a> | ||
| 261 | <a href="#c-SHA7">branch side</a> | 283 | <a href="#c-SHA7">branch side</a> | ||
| 262 | <a href="#c-SHA7">tag v1.0.0</a> | 284 | <a href="#c-SHA7">tag v1.0.0</a> | ||
| 263 | </nav> | 285 | </nav> | ||
| 264 | </section> | 286 | </section> | ||
| 265 | - | </details> | |||
| 266 | </aside> | 287 | </aside> | ||
| 267 | </div> | 288 | </div> | ||
| 268 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 289 | + | <footer class="foot"> |
| 290 | + | <span>ermite devel</span> | |||
| 291 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 292 | + | <span class="foot-keys"> | |||
| 293 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 294 | + | </footer> | |||
| 295 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 296 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 297 | + | </span> | |||
| 298 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 299 | + | </span> | |||
| 300 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 301 | + | </span> | |||
| 302 | + | <span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Adiffs&view=commits&ref=main&head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |||
| 303 | + | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |||
| 304 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |||
| 305 | + | gap -> ermiteLive.reload()"> | |||
| 306 | + | </span> | |||
| 307 | + | </div> | |||
| 308 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 269 | <div class="panel-head"> | 309 | <div class="panel-head"> | ||
| 270 | - | <span>keys</span> | 310 | + | <h2>Keyboard shortcuts</h2> |
| 271 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 311 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 312 | + | <kbd>esc</kbd> close</button> | |||
| 272 | </div> | 313 | </div> | ||
| 273 | <dl class="keys"> | 314 | <dl class="keys"> | ||
| 274 | <div> | 315 | <div> | ||
| 275 | <dt> | 316 | <dt> | ||
| 276 | - | <kbd>j</kbd> | 317 | + | <kbd>ctrl k</kbd> |
| 277 | - | <kbd>k</kbd> | 318 | + | <kbd>:</kbd> |
| 278 | </dt> | 319 | </dt> | ||
| 279 | - | <dd>move cursor down / up</dd> | 320 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 280 | </div> | 321 | </div> | ||
| 281 | <div> | 322 | <div> | ||
| 282 | <dt> | 323 | <dt> | ||
| 283 | - | <kbd>enter</kbd> | 324 | + | <kbd>?</kbd> |
| 284 | </dt> | 325 | </dt> | ||
| 285 | - | <dd>open row under cursor</dd> | 326 | + | <dd>this list</dd> |
| 286 | </div> | 327 | </div> | ||
| 287 | <div> | 328 | <div> | ||
| 288 | <dt> | 329 | <dt> | ||
| 289 | - | <kbd>g</kbd> | 330 | + | <kbd>j</kbd> |
| 290 | - | <kbd>g</kbd> / <kbd>G</kbd> | 331 | + | <kbd>k</kbd> |
| 291 | </dt> | 332 | </dt> | ||
| 292 | - | <dd>first / last row</dd> | 333 | + | <dd>move the row cursor down / up</dd> |
| 293 | </div> | 334 | </div> | ||
| 294 | <div> | 335 | <div> | ||
| 295 | <dt> | 336 | <dt> | ||
| 296 | - | <kbd>/</kbd> | 337 | + | <kbd>enter</kbd> |
| 297 | </dt> | 338 | </dt> | ||
| 298 | - | <dd>focus the filter</dd> | 339 | + | <dd>open the row under the cursor</dd> |
| 299 | </div> | 340 | </div> | ||
| 300 | <div> | 341 | <div> | ||
| 301 | <dt> | 342 | <dt> | ||
| 302 | - | <kbd>g h</kbd> | 343 | + | <kbd>g g</kbd> |
| 303 | - | <kbd>g t</kbd> | 344 | + | <kbd>G</kbd> |
| 304 | </dt> | 345 | </dt> | ||
| 305 | - | <dd>go home / tree</dd> | 346 | + | <dd>first / last row</dd> |
| 306 | </div> | 347 | </div> | ||
| 307 | <div> | 348 | <div> | ||
| 308 | <dt> | 349 | <dt> | ||
| 309 | - | <kbd>g c</kbd> | 350 | + | <kbd>/</kbd> |
| 310 | - | <kbd>g r</kbd> | |||
| 311 | </dt> | 351 | </dt> | ||
| 312 | - | <dd>go commits / refs</dd> | 352 | + | <dd>filter the list</dd> |
| 313 | </div> | 353 | </div> | ||
| 314 | <div> | 354 | <div> | ||
| 315 | <dt> | 355 | <dt> | ||
| 316 | <kbd>b</kbd> | 356 | <kbd>b</kbd> | ||
| 317 | </dt> | 357 | </dt> | ||
| 318 | - | <dd>switch ref (branches, tags)</dd> | 358 | + | <dd>switch branch or tag</dd> |
| 319 | - | </div> | |||
| 320 | - | <div> | |||
| 321 | - | <dt> | |||
| 322 | - | <kbd>:</kbd> | |||
| 323 | - | <kbd>f</kbd> | |||
| 324 | - | </dt> | |||
| 325 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 326 | - | </div> | |||
| 327 | - | <div> | |||
| 328 | - | <dt> | |||
| 329 | - | <kbd>y</kbd> | |||
| 330 | - | </dt> | |||
| 331 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 332 | </div> | 359 | </div> | ||
| 333 | <div> | 360 | <div> | ||
| 334 | <dt> | 361 | <dt> | ||
| @@ -336,15 +363,21 @@ | |||||
| 336 | <kbd>t</kbd> | 363 | <kbd>t</kbd> | ||
| 337 | <kbd>c</kbd> | 364 | <kbd>c</kbd> | ||
| 338 | <kbd>r</kbd> | 365 | <kbd>r</kbd> | ||
| 366 | + | <kbd>m</kbd> | |||
| 339 | </dt> | 367 | </dt> | ||
| 340 | - | <dd>repo sections (when shown)</dd> | 368 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 341 | </div> | 369 | </div> | ||
| 342 | <div> | 370 | <div> | ||
| 343 | <dt> | 371 | <dt> | ||
| 344 | - | <kbd>m</kbd> | 372 | + | <kbd>g h</kbd> |
| 345 | - | <kbd>g m</kbd> | 373 | + | </dt> |
| 374 | + | <dd>home</dd> | |||
| 375 | + | </div> | |||
| 376 | + | <div> | |||
| 377 | + | <dt> | |||
| 378 | + | <kbd>y</kbd> | |||
| 346 | </dt> | 379 | </dt> | ||
| 347 | - | <dd>merge requests (when shown)</dd> | 380 | + | <dd>copy a permalink pinned to the commit</dd> |
| 348 | </div> | 381 | </div> | ||
| 349 | <div> | 382 | <div> | ||
| 350 | <dt> | 383 | <dt> | ||
| @@ -364,19 +397,21 @@ | |||||
| 364 | <dt> | 397 | <dt> | ||
| 365 | <kbd>esc</kbd> | 398 | <kbd>esc</kbd> | ||
| 366 | </dt> | 399 | </dt> | ||
| 367 | - | <dd>close panel</dd> | 400 | + | <dd>close a dialog, leave a field</dd> |
| 368 | </div> | 401 | </div> | ||
| 369 | </dl> | 402 | </dl> | ||
| 370 | </dialog> | 403 | </dialog> | ||
| 371 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 404 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 372 | <div class="panel-head"> | 405 | <div class="panel-head"> | ||
| 373 | - | <label for="refs-filter">ref></label> | 406 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 374 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 407 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 375 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 408 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 409 | + | <kbd>esc</kbd> | |||
| 410 | + | </button> | |||
| 376 | </div> | 411 | </div> | ||
| 377 | <div class="panel-cols" data-picklist> | 412 | <div class="panel-cols" data-picklist> | ||
| 378 | <section aria-label="Branches"> | 413 | <section aria-label="Branches"> | ||
| 379 | - | <h2>branches <span class="mute">2</span> | 414 | + | <h2>Branches <span class="count">2</span> |
| 380 | </h2> | 415 | </h2> | ||
| 381 | <ul> | 416 | <ul> | ||
| 382 | <li> | 417 | <li> | ||
| @@ -390,7 +425,7 @@ | |||||
| 390 | </ul> | 425 | </ul> | ||
| 391 | </section> | 426 | </section> | ||
| 392 | <section aria-label="Tags"> | 427 | <section aria-label="Tags"> | ||
| 393 | - | <h2>tags <span class="mute">1</span> | 428 | + | <h2>Tags <span class="count">1</span> |
| 394 | </h2> | 429 | </h2> | ||
| 395 | <ul> | 430 | <ul> | ||
| 396 | <li> | 431 | <li> | ||
| @@ -401,113 +436,130 @@ | |||||
| 401 | </section> | 436 | </section> | ||
| 402 | </div> | 437 | </div> | ||
| 403 | </dialog> | 438 | </dialog> | ||
| 404 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 439 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 405 | <div class="panel-head"> | 440 | <div class="panel-head"> | ||
| 406 | - | <label for="cmd-q" class="hi">:</label> | 441 | + | <label for="cmd-q" class="sr">go to</label> |
| 407 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 442 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 408 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 443 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 409 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 444 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 410 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 445 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 411 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 446 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 412 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 447 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 413 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 448 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 414 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 449 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 450 | + | <kbd>esc</kbd> | |||
| 451 | + | </button> | |||
| 415 | </div> | 452 | </div> | ||
| 416 | - | <ul id="cmd-list" class="cmd-list"> | 453 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 417 | <li data-k="go home" data-cursor> | 454 | <li data-k="go home" data-cursor> | ||
| 418 | <a href="/" tabindex="-1"> | 455 | <a href="/" tabindex="-1"> | ||
| 419 | - | <span class="k">go</span>home</a> | 456 | + | <span class="lbl">home</span> |
| 457 | + | <span class="k">go</span> | |||
| 458 | + | </a> | |||
| 420 | </li> | 459 | </li> | ||
| 421 | <li data-k="section summary"> | 460 | <li data-k="section summary"> | ||
| 422 | <a href="/diffs" tabindex="-1"> | 461 | <a href="/diffs" tabindex="-1"> | ||
| 423 | - | <span class="k">section</span>summary</a> | 462 | + | <span class="lbl">summary</span> |
| 463 | + | <span class="k">section</span> | |||
| 464 | + | </a> | |||
| 424 | </li> | 465 | </li> | ||
| 425 | - | <li data-k="section tree"> | 466 | + | <li data-k="section files"> |
| 426 | <a href="/diffs/tree/main" tabindex="-1"> | 467 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 427 | - | <span class="k">section</span>tree</a> | 468 | + | <span class="lbl">files</span> |
| 469 | + | <span class="k">section</span> | |||
| 470 | + | </a> | |||
| 428 | </li> | 471 | </li> | ||
| 429 | <li data-k="section commits"> | 472 | <li data-k="section commits"> | ||
| 430 | <a href="/diffs/commits/main" tabindex="-1"> | 473 | <a href="/diffs/commits/main" tabindex="-1"> | ||
| 431 | - | <span class="k">section</span>commits</a> | 474 | + | <span class="lbl">commits</span> |
| 475 | + | <span class="k">section</span> | |||
| 476 | + | </a> | |||
| 432 | </li> | 477 | </li> | ||
| 433 | - | <li data-k="section refs"> | 478 | + | <li data-k="section branches & tags"> |
| 434 | <a href="/diffs/refs" tabindex="-1"> | 479 | <a href="/diffs/refs" tabindex="-1"> | ||
| 435 | - | <span class="k">section</span>refs</a> | 480 | + | <span class="lbl">branches & tags</span> |
| 481 | + | <span class="k">section</span> | |||
| 482 | + | </a> | |||
| 436 | </li> | 483 | </li> | ||
| 437 | <li data-k="section merge requests"> | 484 | <li data-k="section merge requests"> | ||
| 438 | <a href="/diffs/merge-requests" tabindex="-1"> | 485 | <a href="/diffs/merge-requests" tabindex="-1"> | ||
| 439 | - | <span class="k">section</span>merge requests</a> | 486 | + | <span class="lbl">merge requests</span> |
| 487 | + | <span class="k">section</span> | |||
| 488 | + | </a> | |||
| 440 | </li> | 489 | </li> | ||
| 441 | <li data-k="branch main"> | 490 | <li data-k="branch main"> | ||
| 442 | <a href="/diffs/tree/main" tabindex="-1"> | 491 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 443 | - | <span class="k">branch</span>main</a> | 492 | + | <span class="lbl">main</span> |
| 493 | + | <span class="k">branch</span> | |||
| 494 | + | </a> | |||
| 444 | </li> | 495 | </li> | ||
| 445 | <li data-k="branch side"> | 496 | <li data-k="branch side"> | ||
| 446 | <a href="/diffs/tree/side" tabindex="-1"> | 497 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 447 | - | <span class="k">branch</span>side</a> | 498 | + | <span class="lbl">side</span> |
| 499 | + | <span class="k">branch</span> | |||
| 500 | + | </a> | |||
| 448 | </li> | 501 | </li> | ||
| 449 | <li data-k="tag v1.0.0"> | 502 | <li data-k="tag v1.0.0"> | ||
| 450 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 503 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 451 | - | <span class="k">tag</span>v1.0.0</a> | 504 | + | <span class="lbl">v1.0.0</span> |
| 505 | + | <span class="k">tag</span> | |||
| 506 | + | </a> | |||
| 452 | </li> | 507 | </li> | ||
| 453 | <li data-k="repo long"> | 508 | <li data-k="repo long"> | ||
| 454 | <a href="/long" tabindex="-1"> | 509 | <a href="/long" tabindex="-1"> | ||
| 455 | - | <span class="k">repo</span>long</a> | 510 | + | <span class="lbl">long</span> |
| 511 | + | <span class="k">repo</span> | |||
| 512 | + | </a> | |||
| 456 | </li> | 513 | </li> | ||
| 457 | </ul> | 514 | </ul> | ||
| 515 | + | <p class="panel-foot"> | |||
| 516 | + | <span> | |||
| 517 | + | <kbd>↑</kbd> | |||
| 518 | + | <kbd>↓</kbd> move</span> | |||
| 519 | + | <span> | |||
| 520 | + | <kbd>enter</kbd> open</span> | |||
| 521 | + | <span> | |||
| 522 | + | <kbd>esc</kbd> close</span> | |||
| 523 | + | </p> | |||
| 458 | </dialog> | 524 | </dialog> | ||
| 459 | - | <footer class="bar status" aria-label="Status"> | 525 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 460 | - | <span class="mode">commits</span> | |||
| 461 | - | <span class="st-ctx">main · 8 commits</span> | |||
| 462 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 463 | - | <span class="hi">:</span> go to…</button> | |||
| 464 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 465 | - | </span> | |||
| 466 | - | <span class="st-hints">j/k move · enter open · / filter · ? help</span> | |||
| 467 | - | <span class="st-src"> | |||
| 468 | - | <span class="st-ver">ermite devel · </span> | |||
| 469 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 470 | - | </span> | |||
| 471 | - | <span class="st-live" data-state="offline" role="status" hx-sse:connect="/-/events?topic=repo%3Adiffs&view=commits&ref=main&head=SHA40" hx-swap="none" :text="data.state" :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |||
| 472 | - | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |||
| 473 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |||
| 474 | - | gap -> ermiteLive.reload()">offline</span> | |||
| 475 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/8</span> | |||
| 476 | - | </footer> | |||
| 477 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 478 | if (q('dialog[open]').count) return; | 526 | if (q('dialog[open]').count) return; | ||
| 479 | let rows = q('[data-rows] > :not([hidden])').arr(); | 527 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 480 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 528 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 481 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 529 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 482 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 530 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 483 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 531 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 532 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 533 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 484 | })(1); | 534 | })(1); | ||
| 485 | - | k-k from:body -> ((d) => { | 535 | + | k-k from:body -> (async (d) => { |
| 486 | if (q('dialog[open]').count) return; | 536 | if (q('dialog[open]').count) return; | ||
| 487 | let rows = q('[data-rows] > :not([hidden])').arr(); | 537 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 488 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 538 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 489 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 539 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 490 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 540 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 491 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 541 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 542 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 543 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 492 | })(-1); | 544 | })(-1); | ||
| 493 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 545 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 494 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 546 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 495 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 547 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 496 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 548 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 497 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 549 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 498 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 550 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 499 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 551 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 500 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 552 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 501 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 553 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 502 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 554 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 503 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 555 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 504 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 556 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 505 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 557 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 506 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 558 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 507 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 559 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 508 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 560 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 509 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 561 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 510 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 562 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 511 | k-next from:body -> { | 563 | k-next from:body -> { | ||
| 512 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 564 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 513 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 565 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/empty.html +113 -87
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>quiet · example.test</title> | 7 | <title>quiet · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="" data-sel=""> | 20 | + | <body data-ref="" data-sha="" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -29,23 +33,33 @@ | |||||
| 29 | </li> | 33 | </li> | ||
| 30 | </ol> | 34 | </ol> | ||
| 31 | </nav> | 35 | </nav> | ||
| 36 | + | <div class="hdr-acts"> | |||
| 37 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 38 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 39 | + | </span> | |||
| 40 | + | <span class="search-label">Go to…</span> | |||
| 41 | + | <kbd>ctrl k</kbd> | |||
| 42 | + | </button> | |||
| 43 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 44 | + | </div> | |||
| 45 | + | </div> | |||
| 32 | </header> | 46 | </header> | ||
| 33 | <div class="work"> | 47 | <div class="work"> | ||
| 34 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 48 | + | <main id="main" tabindex="-1"> |
| 35 | <section class="state" aria-labelledby="state-title"> | 49 | <section class="state" aria-labelledby="state-title"> | ||
| 36 | - | <h1 id="state-title"> | 50 | + | <p class="eyebrow">empty</p> |
| 37 | - | <span class="code">empty</span> quiet is empty</h1> | 51 | + | <h1 id="state-title">quiet is empty</h1> |
| 38 | <p class="dim">no commits yet. push a branch to create one.</p> | 52 | <p class="dim">no commits yet. push a branch to create one.</p> | ||
| 39 | <div class="clone" id="clone-quiet"> | 53 | <div class="clone" id="clone-quiet"> | ||
| 40 | <div class="clone-line"> | 54 | <div class="clone-line"> | ||
| 41 | <span class="mute" aria-hidden="true">$ </span> | 55 | <span class="mute" aria-hidden="true">$ </span> | ||
| 42 | <code>git remote add origin http://example.test/quiet.git</code> | 56 | <code>git remote add origin http://example.test/quiet.git</code> | ||
| 43 | - | <button type="button" class="copy" data-label="[copy]" aria-label="copy: git remote add origin http://example.test/quiet.git" hx-on:click="navigator.clipboard?.writeText(closest('.clone-line').querySelector('code').textContent); this.textContent = '[copied]'; await timeout('1500ms'); this.textContent = this.dataset.label">[copy]</button> | 57 | + | <button type="button" class="btn copy" data-label="Copy" aria-label="copy: git remote add origin http://example.test/quiet.git" hx-on:click="navigator.clipboard?.writeText(closest('.clone-line').querySelector('code').textContent); this.textContent = 'Copied'; await timeout('1500ms'); this.textContent = this.dataset.label">Copy</button> |
| 44 | </div> | 58 | </div> | ||
| 45 | <div class="clone-line"> | 59 | <div class="clone-line"> | ||
| 46 | <span class="mute" aria-hidden="true">$ </span> | 60 | <span class="mute" aria-hidden="true">$ </span> | ||
| 47 | <code>git push -u origin main</code> | 61 | <code>git push -u origin main</code> | ||
| 48 | - | <button type="button" class="copy" data-label="[copy]" aria-label="copy: git push -u origin main" hx-on:click="navigator.clipboard?.writeText(closest('.clone-line').querySelector('code').textContent); this.textContent = '[copied]'; await timeout('1500ms'); this.textContent = this.dataset.label">[copy]</button> | 62 | + | <button type="button" class="btn copy" data-label="Copy" aria-label="copy: git push -u origin main" hx-on:click="navigator.clipboard?.writeText(closest('.clone-line').querySelector('code').textContent); this.textContent = 'Copied'; await timeout('1500ms'); this.textContent = this.dataset.label">Copy</button> |
| 49 | </div> | 63 | </div> | ||
| 50 | </div> | 64 | </div> | ||
| 51 | <p class="state-back"> | 65 | <p class="state-back"> | ||
| @@ -54,70 +68,71 @@ | |||||
| 54 | </section> | 68 | </section> | ||
| 55 | </main> | 69 | </main> | ||
| 56 | </div> | 70 | </div> | ||
| 57 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 71 | + | <footer class="foot"> |
| 72 | + | <span>ermite devel</span> | |||
| 73 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 74 | + | <span class="foot-keys"> | |||
| 75 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 76 | + | </footer> | |||
| 77 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 78 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 79 | + | </span> | |||
| 80 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 81 | + | </span> | |||
| 82 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 83 | + | </span> | |||
| 84 | + | </div> | |||
| 85 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 58 | <div class="panel-head"> | 86 | <div class="panel-head"> | ||
| 59 | - | <span>keys</span> | 87 | + | <h2>Keyboard shortcuts</h2> |
| 60 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 88 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 89 | + | <kbd>esc</kbd> close</button> | |||
| 61 | </div> | 90 | </div> | ||
| 62 | <dl class="keys"> | 91 | <dl class="keys"> | ||
| 63 | <div> | 92 | <div> | ||
| 64 | <dt> | 93 | <dt> | ||
| 65 | - | <kbd>j</kbd> | 94 | + | <kbd>ctrl k</kbd> |
| 66 | - | <kbd>k</kbd> | 95 | + | <kbd>:</kbd> |
| 67 | </dt> | 96 | </dt> | ||
| 68 | - | <dd>move cursor down / up</dd> | 97 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 69 | </div> | 98 | </div> | ||
| 70 | <div> | 99 | <div> | ||
| 71 | <dt> | 100 | <dt> | ||
| 72 | - | <kbd>enter</kbd> | 101 | + | <kbd>?</kbd> |
| 73 | </dt> | 102 | </dt> | ||
| 74 | - | <dd>open row under cursor</dd> | 103 | + | <dd>this list</dd> |
| 75 | </div> | 104 | </div> | ||
| 76 | <div> | 105 | <div> | ||
| 77 | <dt> | 106 | <dt> | ||
| 78 | - | <kbd>g</kbd> | 107 | + | <kbd>j</kbd> |
| 79 | - | <kbd>g</kbd> / <kbd>G</kbd> | 108 | + | <kbd>k</kbd> |
| 80 | </dt> | 109 | </dt> | ||
| 81 | - | <dd>first / last row</dd> | 110 | + | <dd>move the row cursor down / up</dd> |
| 82 | </div> | 111 | </div> | ||
| 83 | <div> | 112 | <div> | ||
| 84 | <dt> | 113 | <dt> | ||
| 85 | - | <kbd>/</kbd> | 114 | + | <kbd>enter</kbd> |
| 86 | </dt> | 115 | </dt> | ||
| 87 | - | <dd>focus the filter</dd> | 116 | + | <dd>open the row under the cursor</dd> |
| 88 | </div> | 117 | </div> | ||
| 89 | <div> | 118 | <div> | ||
| 90 | <dt> | 119 | <dt> | ||
| 91 | - | <kbd>g h</kbd> | 120 | + | <kbd>g g</kbd> |
| 92 | - | <kbd>g t</kbd> | 121 | + | <kbd>G</kbd> |
| 93 | </dt> | 122 | </dt> | ||
| 94 | - | <dd>go home / tree</dd> | 123 | + | <dd>first / last row</dd> |
| 95 | </div> | 124 | </div> | ||
| 96 | <div> | 125 | <div> | ||
| 97 | <dt> | 126 | <dt> | ||
| 98 | - | <kbd>g c</kbd> | 127 | + | <kbd>/</kbd> |
| 99 | - | <kbd>g r</kbd> | |||
| 100 | </dt> | 128 | </dt> | ||
| 101 | - | <dd>go commits / refs</dd> | 129 | + | <dd>filter the list</dd> |
| 102 | </div> | 130 | </div> | ||
| 103 | <div> | 131 | <div> | ||
| 104 | <dt> | 132 | <dt> | ||
| 105 | <kbd>b</kbd> | 133 | <kbd>b</kbd> | ||
| 106 | </dt> | 134 | </dt> | ||
| 107 | - | <dd>switch ref (branches, tags)</dd> | 135 | + | <dd>switch branch or tag</dd> |
| 108 | - | </div> | |||
| 109 | - | <div> | |||
| 110 | - | <dt> | |||
| 111 | - | <kbd>:</kbd> | |||
| 112 | - | <kbd>f</kbd> | |||
| 113 | - | </dt> | |||
| 114 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 115 | - | </div> | |||
| 116 | - | <div> | |||
| 117 | - | <dt> | |||
| 118 | - | <kbd>y</kbd> | |||
| 119 | - | </dt> | |||
| 120 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 121 | </div> | 136 | </div> | ||
| 122 | <div> | 137 | <div> | ||
| 123 | <dt> | 138 | <dt> | ||
| @@ -125,15 +140,21 @@ | |||||
| 125 | <kbd>t</kbd> | 140 | <kbd>t</kbd> | ||
| 126 | <kbd>c</kbd> | 141 | <kbd>c</kbd> | ||
| 127 | <kbd>r</kbd> | 142 | <kbd>r</kbd> | ||
| 143 | + | <kbd>m</kbd> | |||
| 128 | </dt> | 144 | </dt> | ||
| 129 | - | <dd>repo sections (when shown)</dd> | 145 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 130 | </div> | 146 | </div> | ||
| 131 | <div> | 147 | <div> | ||
| 132 | <dt> | 148 | <dt> | ||
| 133 | - | <kbd>m</kbd> | 149 | + | <kbd>g h</kbd> |
| 134 | - | <kbd>g m</kbd> | |||
| 135 | </dt> | 150 | </dt> | ||
| 136 | - | <dd>merge requests (when shown)</dd> | 151 | + | <dd>home</dd> |
| 152 | + | </div> | |||
| 153 | + | <div> | |||
| 154 | + | <dt> | |||
| 155 | + | <kbd>y</kbd> | |||
| 156 | + | </dt> | |||
| 157 | + | <dd>copy a permalink pinned to the commit</dd> | |||
| 137 | </div> | 158 | </div> | ||
| 138 | <div> | 159 | <div> | ||
| 139 | <dt> | 160 | <dt> | ||
| @@ -153,81 +174,86 @@ | |||||
| 153 | <dt> | 174 | <dt> | ||
| 154 | <kbd>esc</kbd> | 175 | <kbd>esc</kbd> | ||
| 155 | </dt> | 176 | </dt> | ||
| 156 | - | <dd>close panel</dd> | 177 | + | <dd>close a dialog, leave a field</dd> |
| 157 | </div> | 178 | </div> | ||
| 158 | </dl> | 179 | </dl> | ||
| 159 | </dialog> | 180 | </dialog> | ||
| 160 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 181 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 161 | <div class="panel-head"> | 182 | <div class="panel-head"> | ||
| 162 | - | <label for="cmd-q" class="hi">:</label> | 183 | + | <label for="cmd-q" class="sr">go to</label> |
| 163 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 184 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 164 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 185 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 165 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 186 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 166 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 187 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 167 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 188 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 168 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 189 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 169 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 190 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 170 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 191 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 192 | + | <kbd>esc</kbd> | |||
| 193 | + | </button> | |||
| 171 | </div> | 194 | </div> | ||
| 172 | - | <ul id="cmd-list" class="cmd-list"> | 195 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 173 | <li data-k="go home" data-cursor> | 196 | <li data-k="go home" data-cursor> | ||
| 174 | <a href="/" tabindex="-1"> | 197 | <a href="/" tabindex="-1"> | ||
| 175 | - | <span class="k">go</span>home</a> | 198 | + | <span class="lbl">home</span> |
| 199 | + | <span class="k">go</span> | |||
| 200 | + | </a> | |||
| 176 | </li> | 201 | </li> | ||
| 177 | <li data-k="repo demo"> | 202 | <li data-k="repo demo"> | ||
| 178 | <a href="/demo" tabindex="-1"> | 203 | <a href="/demo" tabindex="-1"> | ||
| 179 | - | <span class="k">repo</span>demo</a> | 204 | + | <span class="lbl">demo</span> |
| 205 | + | <span class="k">repo</span> | |||
| 206 | + | </a> | |||
| 180 | </li> | 207 | </li> | ||
| 181 | </ul> | 208 | </ul> | ||
| 209 | + | <p class="panel-foot"> | |||
| 210 | + | <span> | |||
| 211 | + | <kbd>↑</kbd> | |||
| 212 | + | <kbd>↓</kbd> move</span> | |||
| 213 | + | <span> | |||
| 214 | + | <kbd>enter</kbd> open</span> | |||
| 215 | + | <span> | |||
| 216 | + | <kbd>esc</kbd> close</span> | |||
| 217 | + | </p> | |||
| 182 | </dialog> | 218 | </dialog> | ||
| 183 | - | <footer class="bar status" aria-label="Status"> | 219 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 184 | - | <span class="mode">empty</span> | |||
| 185 | - | <span class="st-ctx">empty repository · 0 commits</span> | |||
| 186 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 187 | - | <span class="hi">:</span> go to…</button> | |||
| 188 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 189 | - | </span> | |||
| 190 | - | <span class="st-hints">g h home · ? help</span> | |||
| 191 | - | <span class="st-src"> | |||
| 192 | - | <span class="st-ver">ermite devel · </span> | |||
| 193 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 194 | - | </span> | |||
| 195 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 196 | - | </footer> | |||
| 197 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 198 | if (q('dialog[open]').count) return; | 220 | if (q('dialog[open]').count) return; | ||
| 199 | let rows = q('[data-rows] > :not([hidden])').arr(); | 221 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 200 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 222 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 201 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 223 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 202 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 224 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 203 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 225 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 226 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 227 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 204 | })(1); | 228 | })(1); | ||
| 205 | - | k-k from:body -> ((d) => { | 229 | + | k-k from:body -> (async (d) => { |
| 206 | if (q('dialog[open]').count) return; | 230 | if (q('dialog[open]').count) return; | ||
| 207 | let rows = q('[data-rows] > :not([hidden])').arr(); | 231 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 208 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 232 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 209 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 233 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 210 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 234 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 211 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 235 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 236 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 237 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 212 | })(-1); | 238 | })(-1); | ||
| 213 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 239 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 214 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 240 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 215 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 241 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 216 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 242 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 217 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 243 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 218 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 244 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 219 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 245 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 220 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 246 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 221 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 247 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 222 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 248 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 223 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 249 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 224 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 250 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 225 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 251 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 226 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 252 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 227 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 253 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 228 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 254 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 229 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 255 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 230 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 256 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 231 | k-next from:body -> { | 257 | k-next from:body -> { | ||
| 232 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 258 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 233 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 259 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/home.html +119 -101
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>example.test</title> | 7 | <title>example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,29 +17,31 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="" data-sel=""> | 20 | + | <body data-ref="" data-sha="" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 25 | - | <nav class="crumbs" aria-label="Path"> | 27 | + | <span>example.test</span> |
| 26 | - | <ol> | 28 | + | </a> |
| 27 | - | </ol> | 29 | + | <div class="hdr-acts"> |
| 28 | - | </nav> | 30 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> |
| 29 | - | <nav class="secs" aria-label="Sections"> | 31 | + | <span class="ico i-search" aria-hidden="true"> |
| 30 | - | <ul> | 32 | + | </span> |
| 31 | - | <li> | 33 | + | <span class="search-label">Go to…</span> |
| 32 | - | <a href="/" data-key="h" aria-keyshortcuts="g h" aria-current="page">home</a> | 34 | + | <kbd>ctrl k</kbd> |
| 33 | - | </li> | 35 | + | </button> |
| 34 | - | </ul> | 36 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> |
| 35 | - | </nav> | 37 | + | </div> |
| 38 | + | </div> | |||
| 36 | </header> | 39 | </header> | ||
| 37 | <div class="work"> | 40 | <div class="work"> | ||
| 38 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 41 | + | <main id="main" tabindex="-1"> |
| 39 | <div class="filter"> | 42 | <div class="filter"> | ||
| 40 | - | <label for="home-filter">filter></label> | 43 | + | <label for="home-filter" class="sr">filter</label> |
| 41 | - | <input id="home-filter" type="search" data-filter placeholder="name or description ( / )" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#repo-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#repo-rows > tr:not([hidden])')"> | 44 | + | <input id="home-filter" type="search" data-filter placeholder="Filter repositories…" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#repo-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#repo-rows > tr:not([hidden])')"> |
| 42 | </div> | 45 | </div> | ||
| 43 | <table class="list home" aria-label="Repositories"> | 46 | <table class="list home" aria-label="Repositories"> | ||
| 44 | <thead> | 47 | <thead> | ||
| @@ -90,70 +93,76 @@ | |||||
| 90 | </table> | 93 | </table> | ||
| 91 | </main> | 94 | </main> | ||
| 92 | </div> | 95 | </div> | ||
| 93 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 96 | + | <footer class="foot"> |
| 97 | + | <span>ermite devel</span> | |||
| 98 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 99 | + | <span class="foot-keys"> | |||
| 100 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 101 | + | </footer> | |||
| 102 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 103 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 104 | + | </span> | |||
| 105 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 106 | + | </span> | |||
| 107 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 108 | + | </span> | |||
| 109 | + | <span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=home" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |||
| 110 | + | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |||
| 111 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |||
| 112 | + | gap -> ermiteLive.reload()"> | |||
| 113 | + | </span> | |||
| 114 | + | </div> | |||
| 115 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 94 | <div class="panel-head"> | 116 | <div class="panel-head"> | ||
| 95 | - | <span>keys</span> | 117 | + | <h2>Keyboard shortcuts</h2> |
| 96 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 118 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 119 | + | <kbd>esc</kbd> close</button> | |||
| 97 | </div> | 120 | </div> | ||
| 98 | <dl class="keys"> | 121 | <dl class="keys"> | ||
| 99 | <div> | 122 | <div> | ||
| 100 | <dt> | 123 | <dt> | ||
| 101 | - | <kbd>j</kbd> | 124 | + | <kbd>ctrl k</kbd> |
| 102 | - | <kbd>k</kbd> | 125 | + | <kbd>:</kbd> |
| 103 | </dt> | 126 | </dt> | ||
| 104 | - | <dd>move cursor down / up</dd> | 127 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 105 | </div> | 128 | </div> | ||
| 106 | <div> | 129 | <div> | ||
| 107 | <dt> | 130 | <dt> | ||
| 108 | - | <kbd>enter</kbd> | 131 | + | <kbd>?</kbd> |
| 109 | </dt> | 132 | </dt> | ||
| 110 | - | <dd>open row under cursor</dd> | 133 | + | <dd>this list</dd> |
| 111 | </div> | 134 | </div> | ||
| 112 | <div> | 135 | <div> | ||
| 113 | <dt> | 136 | <dt> | ||
| 114 | - | <kbd>g</kbd> | 137 | + | <kbd>j</kbd> |
| 115 | - | <kbd>g</kbd> / <kbd>G</kbd> | 138 | + | <kbd>k</kbd> |
| 116 | </dt> | 139 | </dt> | ||
| 117 | - | <dd>first / last row</dd> | 140 | + | <dd>move the row cursor down / up</dd> |
| 118 | </div> | 141 | </div> | ||
| 119 | <div> | 142 | <div> | ||
| 120 | <dt> | 143 | <dt> | ||
| 121 | - | <kbd>/</kbd> | 144 | + | <kbd>enter</kbd> |
| 122 | </dt> | 145 | </dt> | ||
| 123 | - | <dd>focus the filter</dd> | 146 | + | <dd>open the row under the cursor</dd> |
| 124 | </div> | 147 | </div> | ||
| 125 | <div> | 148 | <div> | ||
| 126 | <dt> | 149 | <dt> | ||
| 127 | - | <kbd>g h</kbd> | 150 | + | <kbd>g g</kbd> |
| 128 | - | <kbd>g t</kbd> | 151 | + | <kbd>G</kbd> |
| 129 | </dt> | 152 | </dt> | ||
| 130 | - | <dd>go home / tree</dd> | 153 | + | <dd>first / last row</dd> |
| 131 | </div> | 154 | </div> | ||
| 132 | <div> | 155 | <div> | ||
| 133 | <dt> | 156 | <dt> | ||
| 134 | - | <kbd>g c</kbd> | 157 | + | <kbd>/</kbd> |
| 135 | - | <kbd>g r</kbd> | |||
| 136 | </dt> | 158 | </dt> | ||
| 137 | - | <dd>go commits / refs</dd> | 159 | + | <dd>filter the list</dd> |
| 138 | </div> | 160 | </div> | ||
| 139 | <div> | 161 | <div> | ||
| 140 | <dt> | 162 | <dt> | ||
| 141 | <kbd>b</kbd> | 163 | <kbd>b</kbd> | ||
| 142 | </dt> | 164 | </dt> | ||
| 143 | - | <dd>switch ref (branches, tags)</dd> | 165 | + | <dd>switch branch or tag</dd> |
| 144 | - | </div> | |||
| 145 | - | <div> | |||
| 146 | - | <dt> | |||
| 147 | - | <kbd>:</kbd> | |||
| 148 | - | <kbd>f</kbd> | |||
| 149 | - | </dt> | |||
| 150 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 151 | - | </div> | |||
| 152 | - | <div> | |||
| 153 | - | <dt> | |||
| 154 | - | <kbd>y</kbd> | |||
| 155 | - | </dt> | |||
| 156 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 157 | </div> | 166 | </div> | ||
| 158 | <div> | 167 | <div> | ||
| 159 | <dt> | 168 | <dt> | ||
| @@ -161,15 +170,21 @@ | |||||
| 161 | <kbd>t</kbd> | 170 | <kbd>t</kbd> | ||
| 162 | <kbd>c</kbd> | 171 | <kbd>c</kbd> | ||
| 163 | <kbd>r</kbd> | 172 | <kbd>r</kbd> | ||
| 173 | + | <kbd>m</kbd> | |||
| 164 | </dt> | 174 | </dt> | ||
| 165 | - | <dd>repo sections (when shown)</dd> | 175 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 166 | </div> | 176 | </div> | ||
| 167 | <div> | 177 | <div> | ||
| 168 | <dt> | 178 | <dt> | ||
| 169 | - | <kbd>m</kbd> | 179 | + | <kbd>g h</kbd> |
| 170 | - | <kbd>g m</kbd> | |||
| 171 | </dt> | 180 | </dt> | ||
| 172 | - | <dd>merge requests (when shown)</dd> | 181 | + | <dd>home</dd> |
| 182 | + | </div> | |||
| 183 | + | <div> | |||
| 184 | + | <dt> | |||
| 185 | + | <kbd>y</kbd> | |||
| 186 | + | </dt> | |||
| 187 | + | <dd>copy a permalink pinned to the commit</dd> | |||
| 173 | </div> | 188 | </div> | ||
| 174 | <div> | 189 | <div> | ||
| 175 | <dt> | 190 | <dt> | ||
| @@ -189,89 +204,92 @@ | |||||
| 189 | <dt> | 204 | <dt> | ||
| 190 | <kbd>esc</kbd> | 205 | <kbd>esc</kbd> | ||
| 191 | </dt> | 206 | </dt> | ||
| 192 | - | <dd>close panel</dd> | 207 | + | <dd>close a dialog, leave a field</dd> |
| 193 | </div> | 208 | </div> | ||
| 194 | </dl> | 209 | </dl> | ||
| 195 | </dialog> | 210 | </dialog> | ||
| 196 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 211 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 197 | <div class="panel-head"> | 212 | <div class="panel-head"> | ||
| 198 | - | <label for="cmd-q" class="hi">:</label> | 213 | + | <label for="cmd-q" class="sr">go to</label> |
| 199 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 214 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 200 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 215 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 201 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 216 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 202 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 217 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 203 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 218 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 204 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 219 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 205 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 220 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 206 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 221 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 222 | + | <kbd>esc</kbd> | |||
| 223 | + | </button> | |||
| 207 | </div> | 224 | </div> | ||
| 208 | - | <ul id="cmd-list" class="cmd-list"> | 225 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 209 | <li data-k="go home" data-cursor> | 226 | <li data-k="go home" data-cursor> | ||
| 210 | <a href="/" tabindex="-1"> | 227 | <a href="/" tabindex="-1"> | ||
| 211 | - | <span class="k">go</span>home</a> | 228 | + | <span class="lbl">home</span> |
| 229 | + | <span class="k">go</span> | |||
| 230 | + | </a> | |||
| 212 | </li> | 231 | </li> | ||
| 213 | <li data-k="repo demo"> | 232 | <li data-k="repo demo"> | ||
| 214 | <a href="/demo" tabindex="-1"> | 233 | <a href="/demo" tabindex="-1"> | ||
| 215 | - | <span class="k">repo</span>demo</a> | 234 | + | <span class="lbl">demo</span> |
| 235 | + | <span class="k">repo</span> | |||
| 236 | + | </a> | |||
| 216 | </li> | 237 | </li> | ||
| 217 | <li data-k="repo quiet"> | 238 | <li data-k="repo quiet"> | ||
| 218 | <a href="/quiet" tabindex="-1"> | 239 | <a href="/quiet" tabindex="-1"> | ||
| 219 | - | <span class="k">repo</span>quiet</a> | 240 | + | <span class="lbl">quiet</span> |
| 241 | + | <span class="k">repo</span> | |||
| 242 | + | </a> | |||
| 220 | </li> | 243 | </li> | ||
| 221 | </ul> | 244 | </ul> | ||
| 245 | + | <p class="panel-foot"> | |||
| 246 | + | <span> | |||
| 247 | + | <kbd>↑</kbd> | |||
| 248 | + | <kbd>↓</kbd> move</span> | |||
| 249 | + | <span> | |||
| 250 | + | <kbd>enter</kbd> open</span> | |||
| 251 | + | <span> | |||
| 252 | + | <kbd>esc</kbd> close</span> | |||
| 253 | + | </p> | |||
| 222 | </dialog> | 254 | </dialog> | ||
| 223 | - | <footer class="bar status" aria-label="Status"> | 255 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 224 | - | <span class="mode">home</span> | |||
| 225 | - | <span class="st-ctx">2 repos · sort push ▼</span> | |||
| 226 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 227 | - | <span class="hi">:</span> go to…</button> | |||
| 228 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 229 | - | </span> | |||
| 230 | - | <span class="st-hints">j/k move · enter open · / filter · ? help</span> | |||
| 231 | - | <span class="st-src"> | |||
| 232 | - | <span class="st-ver">ermite devel · </span> | |||
| 233 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 234 | - | </span> | |||
| 235 | - | <span class="st-live" data-state="offline" role="status" hx-sse:connect="/-/events?topic=home" hx-swap="none" :text="data.state" :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |||
| 236 | - | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |||
| 237 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |||
| 238 | - | gap -> ermiteLive.reload()">offline</span> | |||
| 239 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/2</span> | |||
| 240 | - | </footer> | |||
| 241 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 242 | if (q('dialog[open]').count) return; | 256 | if (q('dialog[open]').count) return; | ||
| 243 | let rows = q('[data-rows] > :not([hidden])').arr(); | 257 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 244 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 258 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 245 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 259 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 246 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 260 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 247 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 261 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 262 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 263 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 248 | })(1); | 264 | })(1); | ||
| 249 | - | k-k from:body -> ((d) => { | 265 | + | k-k from:body -> (async (d) => { |
| 250 | if (q('dialog[open]').count) return; | 266 | if (q('dialog[open]').count) return; | ||
| 251 | let rows = q('[data-rows] > :not([hidden])').arr(); | 267 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 252 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 268 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 253 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 269 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 254 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 270 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 255 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 271 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 272 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 273 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 256 | })(-1); | 274 | })(-1); | ||
| 257 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 275 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 258 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 276 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 259 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 277 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 260 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 278 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 261 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 279 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 262 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 280 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 263 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 281 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 264 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 282 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 265 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 283 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 266 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 284 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 267 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 285 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 268 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 286 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 269 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 287 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 270 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 288 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 271 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 289 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 272 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 290 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 273 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 291 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 274 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 292 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 275 | k-next from:body -> { | 293 | k-next from:body -> { | ||
| 276 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 294 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 277 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 295 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/not-found-page.html +109 -95
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>404 · example.test</title> | 7 | <title>404 · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,35 +17,38 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="" data-sel=""> | 20 | + | <body data-ref="" data-sha="" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| 28 | - | <span aria-current="page">-</span> | 32 | + | <span aria-current="page">not found</span> |
| 29 | - | </li> | |||
| 30 | - | <li> | |||
| 31 | - | <span aria-current="page">404</span> | |||
| 32 | </li> | 33 | </li> | ||
| 33 | </ol> | 34 | </ol> | ||
| 34 | </nav> | 35 | </nav> | ||
| 35 | - | <nav class="secs" aria-label="Sections"> | 36 | + | <div class="hdr-acts"> |
| 36 | - | <ul> | 37 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> |
| 37 | - | <li> | 38 | + | <span class="ico i-search" aria-hidden="true"> |
| 38 | - | <a href="/" data-key="h" aria-keyshortcuts="g h">home</a> | 39 | + | </span> |
| 39 | - | </li> | 40 | + | <span class="search-label">Go to…</span> |
| 40 | - | </ul> | 41 | + | <kbd>ctrl k</kbd> |
| 41 | - | </nav> | 42 | + | </button> |
| 43 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 44 | + | </div> | |||
| 45 | + | </div> | |||
| 42 | </header> | 46 | </header> | ||
| 43 | <div class="work"> | 47 | <div class="work"> | ||
| 44 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 48 | + | <main id="main" tabindex="-1"> |
| 45 | <section class="state" aria-labelledby="state-title"> | 49 | <section class="state" aria-labelledby="state-title"> | ||
| 46 | - | <h1 id="state-title"> | 50 | + | <p class="eyebrow">404</p> |
| 47 | - | <span class="code">404</span> not found</h1> | 51 | + | <h1 id="state-title">not found</h1> |
| 48 | <p class="dim">no such page, ref or path.</p> | 52 | <p class="dim">no such page, ref or path.</p> | ||
| 49 | <p class="dim">check the URL, or start from the repo list.</p> | 53 | <p class="dim">check the URL, or start from the repo list.</p> | ||
| 50 | <p class="state-back"> | 54 | <p class="state-back"> | ||
| @@ -53,70 +57,71 @@ | |||||
| 53 | </section> | 57 | </section> | ||
| 54 | </main> | 58 | </main> | ||
| 55 | </div> | 59 | </div> | ||
| 56 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 60 | + | <footer class="foot"> |
| 61 | + | <span>ermite devel</span> | |||
| 62 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 63 | + | <span class="foot-keys"> | |||
| 64 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 65 | + | </footer> | |||
| 66 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 67 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 68 | + | </span> | |||
| 69 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 70 | + | </span> | |||
| 71 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 72 | + | </span> | |||
| 73 | + | </div> | |||
| 74 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 57 | <div class="panel-head"> | 75 | <div class="panel-head"> | ||
| 58 | - | <span>keys</span> | 76 | + | <h2>Keyboard shortcuts</h2> |
| 59 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 77 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 78 | + | <kbd>esc</kbd> close</button> | |||
| 60 | </div> | 79 | </div> | ||
| 61 | <dl class="keys"> | 80 | <dl class="keys"> | ||
| 62 | <div> | 81 | <div> | ||
| 63 | <dt> | 82 | <dt> | ||
| 64 | - | <kbd>j</kbd> | 83 | + | <kbd>ctrl k</kbd> |
| 65 | - | <kbd>k</kbd> | 84 | + | <kbd>:</kbd> |
| 66 | </dt> | 85 | </dt> | ||
| 67 | - | <dd>move cursor down / up</dd> | 86 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 68 | </div> | 87 | </div> | ||
| 69 | <div> | 88 | <div> | ||
| 70 | <dt> | 89 | <dt> | ||
| 71 | - | <kbd>enter</kbd> | 90 | + | <kbd>?</kbd> |
| 72 | </dt> | 91 | </dt> | ||
| 73 | - | <dd>open row under cursor</dd> | 92 | + | <dd>this list</dd> |
| 74 | </div> | 93 | </div> | ||
| 75 | <div> | 94 | <div> | ||
| 76 | <dt> | 95 | <dt> | ||
| 77 | - | <kbd>g</kbd> | 96 | + | <kbd>j</kbd> |
| 78 | - | <kbd>g</kbd> / <kbd>G</kbd> | 97 | + | <kbd>k</kbd> |
| 79 | </dt> | 98 | </dt> | ||
| 80 | - | <dd>first / last row</dd> | 99 | + | <dd>move the row cursor down / up</dd> |
| 81 | </div> | 100 | </div> | ||
| 82 | <div> | 101 | <div> | ||
| 83 | <dt> | 102 | <dt> | ||
| 84 | - | <kbd>/</kbd> | 103 | + | <kbd>enter</kbd> |
| 85 | </dt> | 104 | </dt> | ||
| 86 | - | <dd>focus the filter</dd> | 105 | + | <dd>open the row under the cursor</dd> |
| 87 | </div> | 106 | </div> | ||
| 88 | <div> | 107 | <div> | ||
| 89 | <dt> | 108 | <dt> | ||
| 90 | - | <kbd>g h</kbd> | 109 | + | <kbd>g g</kbd> |
| 91 | - | <kbd>g t</kbd> | 110 | + | <kbd>G</kbd> |
| 92 | </dt> | 111 | </dt> | ||
| 93 | - | <dd>go home / tree</dd> | 112 | + | <dd>first / last row</dd> |
| 94 | </div> | 113 | </div> | ||
| 95 | <div> | 114 | <div> | ||
| 96 | <dt> | 115 | <dt> | ||
| 97 | - | <kbd>g c</kbd> | 116 | + | <kbd>/</kbd> |
| 98 | - | <kbd>g r</kbd> | |||
| 99 | </dt> | 117 | </dt> | ||
| 100 | - | <dd>go commits / refs</dd> | 118 | + | <dd>filter the list</dd> |
| 101 | </div> | 119 | </div> | ||
| 102 | <div> | 120 | <div> | ||
| 103 | <dt> | 121 | <dt> | ||
| 104 | <kbd>b</kbd> | 122 | <kbd>b</kbd> | ||
| 105 | </dt> | 123 | </dt> | ||
| 106 | - | <dd>switch ref (branches, tags)</dd> | 124 | + | <dd>switch branch or tag</dd> |
| 107 | - | </div> | |||
| 108 | - | <div> | |||
| 109 | - | <dt> | |||
| 110 | - | <kbd>:</kbd> | |||
| 111 | - | <kbd>f</kbd> | |||
| 112 | - | </dt> | |||
| 113 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 114 | - | </div> | |||
| 115 | - | <div> | |||
| 116 | - | <dt> | |||
| 117 | - | <kbd>y</kbd> | |||
| 118 | - | </dt> | |||
| 119 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 120 | </div> | 125 | </div> | ||
| 121 | <div> | 126 | <div> | ||
| 122 | <dt> | 127 | <dt> | ||
| @@ -124,15 +129,21 @@ | |||||
| 124 | <kbd>t</kbd> | 129 | <kbd>t</kbd> | ||
| 125 | <kbd>c</kbd> | 130 | <kbd>c</kbd> | ||
| 126 | <kbd>r</kbd> | 131 | <kbd>r</kbd> | ||
| 132 | + | <kbd>m</kbd> | |||
| 127 | </dt> | 133 | </dt> | ||
| 128 | - | <dd>repo sections (when shown)</dd> | 134 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 129 | </div> | 135 | </div> | ||
| 130 | <div> | 136 | <div> | ||
| 131 | <dt> | 137 | <dt> | ||
| 132 | - | <kbd>m</kbd> | 138 | + | <kbd>g h</kbd> |
| 133 | - | <kbd>g m</kbd> | 139 | + | </dt> |
| 140 | + | <dd>home</dd> | |||
| 141 | + | </div> | |||
| 142 | + | <div> | |||
| 143 | + | <dt> | |||
| 144 | + | <kbd>y</kbd> | |||
| 134 | </dt> | 145 | </dt> | ||
| 135 | - | <dd>merge requests (when shown)</dd> | 146 | + | <dd>copy a permalink pinned to the commit</dd> |
| 136 | </div> | 147 | </div> | ||
| 137 | <div> | 148 | <div> | ||
| 138 | <dt> | 149 | <dt> | ||
| @@ -152,77 +163,80 @@ | |||||
| 152 | <dt> | 163 | <dt> | ||
| 153 | <kbd>esc</kbd> | 164 | <kbd>esc</kbd> | ||
| 154 | </dt> | 165 | </dt> | ||
| 155 | - | <dd>close panel</dd> | 166 | + | <dd>close a dialog, leave a field</dd> |
| 156 | </div> | 167 | </div> | ||
| 157 | </dl> | 168 | </dl> | ||
| 158 | </dialog> | 169 | </dialog> | ||
| 159 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 170 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 160 | <div class="panel-head"> | 171 | <div class="panel-head"> | ||
| 161 | - | <label for="cmd-q" class="hi">:</label> | 172 | + | <label for="cmd-q" class="sr">go to</label> |
| 162 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 173 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 163 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 174 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 164 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 175 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 165 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 176 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 166 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 177 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 167 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 178 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 168 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 179 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 169 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 180 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 181 | + | <kbd>esc</kbd> | |||
| 182 | + | </button> | |||
| 170 | </div> | 183 | </div> | ||
| 171 | - | <ul id="cmd-list" class="cmd-list"> | 184 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 172 | <li data-k="go home" data-cursor> | 185 | <li data-k="go home" data-cursor> | ||
| 173 | <a href="/" tabindex="-1"> | 186 | <a href="/" tabindex="-1"> | ||
| 174 | - | <span class="k">go</span>home</a> | 187 | + | <span class="lbl">home</span> |
| 188 | + | <span class="k">go</span> | |||
| 189 | + | </a> | |||
| 175 | </li> | 190 | </li> | ||
| 176 | </ul> | 191 | </ul> | ||
| 192 | + | <p class="panel-foot"> | |||
| 193 | + | <span> | |||
| 194 | + | <kbd>↑</kbd> | |||
| 195 | + | <kbd>↓</kbd> move</span> | |||
| 196 | + | <span> | |||
| 197 | + | <kbd>enter</kbd> open</span> | |||
| 198 | + | <span> | |||
| 199 | + | <kbd>esc</kbd> close</span> | |||
| 200 | + | </p> | |||
| 177 | </dialog> | 201 | </dialog> | ||
| 178 | - | <footer class="bar status" aria-label="Status"> | 202 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 179 | - | <span class="mode">state</span> | |||
| 180 | - | <span class="st-ctx">404 · not found</span> | |||
| 181 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 182 | - | <span class="hi">:</span> go to…</button> | |||
| 183 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 184 | - | </span> | |||
| 185 | - | <span class="st-hints">g h home · ? help</span> | |||
| 186 | - | <span class="st-src"> | |||
| 187 | - | <span class="st-ver">ermite devel · </span> | |||
| 188 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 189 | - | </span> | |||
| 190 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 191 | - | </footer> | |||
| 192 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 193 | if (q('dialog[open]').count) return; | 203 | if (q('dialog[open]').count) return; | ||
| 194 | let rows = q('[data-rows] > :not([hidden])').arr(); | 204 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 195 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 205 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 196 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 206 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 197 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 207 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 198 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 208 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 209 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 210 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 199 | })(1); | 211 | })(1); | ||
| 200 | - | k-k from:body -> ((d) => { | 212 | + | k-k from:body -> (async (d) => { |
| 201 | if (q('dialog[open]').count) return; | 213 | if (q('dialog[open]').count) return; | ||
| 202 | let rows = q('[data-rows] > :not([hidden])').arr(); | 214 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 203 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 215 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 204 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 216 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 205 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 217 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 206 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 218 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 219 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 220 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 207 | })(-1); | 221 | })(-1); | ||
| 208 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 222 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 209 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 223 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 210 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 224 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 211 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 225 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 212 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 226 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 213 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 227 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 214 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 228 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 215 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 229 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 216 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 230 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 217 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 231 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 218 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 232 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 219 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 233 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 220 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 234 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 221 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 235 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 222 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 236 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 223 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 237 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 224 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 238 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 225 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 239 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 226 | k-next from:body -> { | 240 | k-next from:body -> { | ||
| 227 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 241 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 228 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 242 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/not-found.html +109 -95
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>404 · example.test</title> | 7 | <title>404 · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,35 +17,38 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="" data-sha="" data-sel=""> | 20 | + | <body data-ref="" data-sha="" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| 28 | - | <span aria-current="page">-</span> | 32 | + | <span aria-current="page">not found</span> |
| 29 | - | </li> | |||
| 30 | - | <li> | |||
| 31 | - | <span aria-current="page">404</span> | |||
| 32 | </li> | 33 | </li> | ||
| 33 | </ol> | 34 | </ol> | ||
| 34 | </nav> | 35 | </nav> | ||
| 35 | - | <nav class="secs" aria-label="Sections"> | 36 | + | <div class="hdr-acts"> |
| 36 | - | <ul> | 37 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> |
| 37 | - | <li> | 38 | + | <span class="ico i-search" aria-hidden="true"> |
| 38 | - | <a href="/" data-key="h" aria-keyshortcuts="g h">home</a> | 39 | + | </span> |
| 39 | - | </li> | 40 | + | <span class="search-label">Go to…</span> |
| 40 | - | </ul> | 41 | + | <kbd>ctrl k</kbd> |
| 41 | - | </nav> | 42 | + | </button> |
| 43 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 44 | + | </div> | |||
| 45 | + | </div> | |||
| 42 | </header> | 46 | </header> | ||
| 43 | <div class="work"> | 47 | <div class="work"> | ||
| 44 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 48 | + | <main id="main" tabindex="-1"> |
| 45 | <section class="state" aria-labelledby="state-title"> | 49 | <section class="state" aria-labelledby="state-title"> | ||
| 46 | - | <h1 id="state-title"> | 50 | + | <p class="eyebrow">404</p> |
| 47 | - | <span class="code">404</span> not found</h1> | 51 | + | <h1 id="state-title">not found</h1> |
| 48 | <p class="dim">no such page, ref or path.</p> | 52 | <p class="dim">no such page, ref or path.</p> | ||
| 49 | <p class="dim">check the URL, or start from the repo list.</p> | 53 | <p class="dim">check the URL, or start from the repo list.</p> | ||
| 50 | <p class="state-back"> | 54 | <p class="state-back"> | ||
| @@ -53,70 +57,71 @@ | |||||
| 53 | </section> | 57 | </section> | ||
| 54 | </main> | 58 | </main> | ||
| 55 | </div> | 59 | </div> | ||
| 56 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 60 | + | <footer class="foot"> |
| 61 | + | <span>ermite devel</span> | |||
| 62 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 63 | + | <span class="foot-keys"> | |||
| 64 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 65 | + | </footer> | |||
| 66 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 67 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 68 | + | </span> | |||
| 69 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 70 | + | </span> | |||
| 71 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 72 | + | </span> | |||
| 73 | + | </div> | |||
| 74 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 57 | <div class="panel-head"> | 75 | <div class="panel-head"> | ||
| 58 | - | <span>keys</span> | 76 | + | <h2>Keyboard shortcuts</h2> |
| 59 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 77 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 78 | + | <kbd>esc</kbd> close</button> | |||
| 60 | </div> | 79 | </div> | ||
| 61 | <dl class="keys"> | 80 | <dl class="keys"> | ||
| 62 | <div> | 81 | <div> | ||
| 63 | <dt> | 82 | <dt> | ||
| 64 | - | <kbd>j</kbd> | 83 | + | <kbd>ctrl k</kbd> |
| 65 | - | <kbd>k</kbd> | 84 | + | <kbd>:</kbd> |
| 66 | </dt> | 85 | </dt> | ||
| 67 | - | <dd>move cursor down / up</dd> | 86 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 68 | </div> | 87 | </div> | ||
| 69 | <div> | 88 | <div> | ||
| 70 | <dt> | 89 | <dt> | ||
| 71 | - | <kbd>enter</kbd> | 90 | + | <kbd>?</kbd> |
| 72 | </dt> | 91 | </dt> | ||
| 73 | - | <dd>open row under cursor</dd> | 92 | + | <dd>this list</dd> |
| 74 | </div> | 93 | </div> | ||
| 75 | <div> | 94 | <div> | ||
| 76 | <dt> | 95 | <dt> | ||
| 77 | - | <kbd>g</kbd> | 96 | + | <kbd>j</kbd> |
| 78 | - | <kbd>g</kbd> / <kbd>G</kbd> | 97 | + | <kbd>k</kbd> |
| 79 | </dt> | 98 | </dt> | ||
| 80 | - | <dd>first / last row</dd> | 99 | + | <dd>move the row cursor down / up</dd> |
| 81 | </div> | 100 | </div> | ||
| 82 | <div> | 101 | <div> | ||
| 83 | <dt> | 102 | <dt> | ||
| 84 | - | <kbd>/</kbd> | 103 | + | <kbd>enter</kbd> |
| 85 | </dt> | 104 | </dt> | ||
| 86 | - | <dd>focus the filter</dd> | 105 | + | <dd>open the row under the cursor</dd> |
| 87 | </div> | 106 | </div> | ||
| 88 | <div> | 107 | <div> | ||
| 89 | <dt> | 108 | <dt> | ||
| 90 | - | <kbd>g h</kbd> | 109 | + | <kbd>g g</kbd> |
| 91 | - | <kbd>g t</kbd> | 110 | + | <kbd>G</kbd> |
| 92 | </dt> | 111 | </dt> | ||
| 93 | - | <dd>go home / tree</dd> | 112 | + | <dd>first / last row</dd> |
| 94 | </div> | 113 | </div> | ||
| 95 | <div> | 114 | <div> | ||
| 96 | <dt> | 115 | <dt> | ||
| 97 | - | <kbd>g c</kbd> | 116 | + | <kbd>/</kbd> |
| 98 | - | <kbd>g r</kbd> | |||
| 99 | </dt> | 117 | </dt> | ||
| 100 | - | <dd>go commits / refs</dd> | 118 | + | <dd>filter the list</dd> |
| 101 | </div> | 119 | </div> | ||
| 102 | <div> | 120 | <div> | ||
| 103 | <dt> | 121 | <dt> | ||
| 104 | <kbd>b</kbd> | 122 | <kbd>b</kbd> | ||
| 105 | </dt> | 123 | </dt> | ||
| 106 | - | <dd>switch ref (branches, tags)</dd> | 124 | + | <dd>switch branch or tag</dd> |
| 107 | - | </div> | |||
| 108 | - | <div> | |||
| 109 | - | <dt> | |||
| 110 | - | <kbd>:</kbd> | |||
| 111 | - | <kbd>f</kbd> | |||
| 112 | - | </dt> | |||
| 113 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 114 | - | </div> | |||
| 115 | - | <div> | |||
| 116 | - | <dt> | |||
| 117 | - | <kbd>y</kbd> | |||
| 118 | - | </dt> | |||
| 119 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 120 | </div> | 125 | </div> | ||
| 121 | <div> | 126 | <div> | ||
| 122 | <dt> | 127 | <dt> | ||
| @@ -124,15 +129,21 @@ | |||||
| 124 | <kbd>t</kbd> | 129 | <kbd>t</kbd> | ||
| 125 | <kbd>c</kbd> | 130 | <kbd>c</kbd> | ||
| 126 | <kbd>r</kbd> | 131 | <kbd>r</kbd> | ||
| 132 | + | <kbd>m</kbd> | |||
| 127 | </dt> | 133 | </dt> | ||
| 128 | - | <dd>repo sections (when shown)</dd> | 134 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 129 | </div> | 135 | </div> | ||
| 130 | <div> | 136 | <div> | ||
| 131 | <dt> | 137 | <dt> | ||
| 132 | - | <kbd>m</kbd> | 138 | + | <kbd>g h</kbd> |
| 133 | - | <kbd>g m</kbd> | 139 | + | </dt> |
| 140 | + | <dd>home</dd> | |||
| 141 | + | </div> | |||
| 142 | + | <div> | |||
| 143 | + | <dt> | |||
| 144 | + | <kbd>y</kbd> | |||
| 134 | </dt> | 145 | </dt> | ||
| 135 | - | <dd>merge requests (when shown)</dd> | 146 | + | <dd>copy a permalink pinned to the commit</dd> |
| 136 | </div> | 147 | </div> | ||
| 137 | <div> | 148 | <div> | ||
| 138 | <dt> | 149 | <dt> | ||
| @@ -152,77 +163,80 @@ | |||||
| 152 | <dt> | 163 | <dt> | ||
| 153 | <kbd>esc</kbd> | 164 | <kbd>esc</kbd> | ||
| 154 | </dt> | 165 | </dt> | ||
| 155 | - | <dd>close panel</dd> | 166 | + | <dd>close a dialog, leave a field</dd> |
| 156 | </div> | 167 | </div> | ||
| 157 | </dl> | 168 | </dl> | ||
| 158 | </dialog> | 169 | </dialog> | ||
| 159 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 170 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 160 | <div class="panel-head"> | 171 | <div class="panel-head"> | ||
| 161 | - | <label for="cmd-q" class="hi">:</label> | 172 | + | <label for="cmd-q" class="sr">go to</label> |
| 162 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 173 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 163 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 174 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 164 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 175 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 165 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 176 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 166 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 177 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 167 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 178 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 168 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 179 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 169 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 180 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 181 | + | <kbd>esc</kbd> | |||
| 182 | + | </button> | |||
| 170 | </div> | 183 | </div> | ||
| 171 | - | <ul id="cmd-list" class="cmd-list"> | 184 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 172 | <li data-k="go home" data-cursor> | 185 | <li data-k="go home" data-cursor> | ||
| 173 | <a href="/" tabindex="-1"> | 186 | <a href="/" tabindex="-1"> | ||
| 174 | - | <span class="k">go</span>home</a> | 187 | + | <span class="lbl">home</span> |
| 188 | + | <span class="k">go</span> | |||
| 189 | + | </a> | |||
| 175 | </li> | 190 | </li> | ||
| 176 | </ul> | 191 | </ul> | ||
| 192 | + | <p class="panel-foot"> | |||
| 193 | + | <span> | |||
| 194 | + | <kbd>↑</kbd> | |||
| 195 | + | <kbd>↓</kbd> move</span> | |||
| 196 | + | <span> | |||
| 197 | + | <kbd>enter</kbd> open</span> | |||
| 198 | + | <span> | |||
| 199 | + | <kbd>esc</kbd> close</span> | |||
| 200 | + | </p> | |||
| 177 | </dialog> | 201 | </dialog> | ||
| 178 | - | <footer class="bar status" aria-label="Status"> | 202 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 179 | - | <span class="mode">state</span> | |||
| 180 | - | <span class="st-ctx">404 · not found</span> | |||
| 181 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 182 | - | <span class="hi">:</span> go to…</button> | |||
| 183 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 184 | - | </span> | |||
| 185 | - | <span class="st-hints">g h home · ? help</span> | |||
| 186 | - | <span class="st-src"> | |||
| 187 | - | <span class="st-ver">ermite devel · </span> | |||
| 188 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 189 | - | </span> | |||
| 190 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 191 | - | </footer> | |||
| 192 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 193 | if (q('dialog[open]').count) return; | 203 | if (q('dialog[open]').count) return; | ||
| 194 | let rows = q('[data-rows] > :not([hidden])').arr(); | 204 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 195 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 205 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 196 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 206 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 197 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 207 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 198 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 208 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 209 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 210 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 199 | })(1); | 211 | })(1); | ||
| 200 | - | k-k from:body -> ((d) => { | 212 | + | k-k from:body -> (async (d) => { |
| 201 | if (q('dialog[open]').count) return; | 213 | if (q('dialog[open]').count) return; | ||
| 202 | let rows = q('[data-rows] > :not([hidden])').arr(); | 214 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 203 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 215 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 204 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 216 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 205 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 217 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 206 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 218 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 219 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 220 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 207 | })(-1); | 221 | })(-1); | ||
| 208 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 222 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 209 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 223 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 210 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 224 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 211 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 225 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 212 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 226 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 213 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 227 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 214 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 228 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 215 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 229 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 216 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 230 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 217 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 231 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 218 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 232 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 219 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 233 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 220 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 234 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 221 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 235 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 222 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 236 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 223 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 237 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 224 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 238 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 225 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 239 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 226 | k-next from:body -> { | 240 | k-next from:body -> { | ||
| 227 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 241 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 228 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 242 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/refs.html +172 -126
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>refs · demo · example.test</title> | 7 | <title>refs · demo · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -32,33 +36,52 @@ | |||||
| 32 | </li> | 36 | </li> | ||
| 33 | </ol> | 37 | </ol> | ||
| 34 | </nav> | 38 | </nav> | ||
| 35 | - | <nav class="secs" aria-label="Sections"> | 39 | + | <div class="hdr-acts"> |
| 40 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 41 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 42 | + | </span> | |||
| 43 | + | <span class="ref-name">main</span> | |||
| 44 | + | <span class="sha">SHA7</span> | |||
| 45 | + | </button> | |||
| 46 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 47 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="search-label">Go to…</span> | |||
| 50 | + | <kbd>ctrl k</kbd> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 53 | + | </div> | |||
| 54 | + | </div> | |||
| 55 | + | <nav class="tabs" aria-label="Sections"> | |||
| 36 | <ul> | 56 | <ul> | ||
| 37 | <li> | 57 | <li> | ||
| 38 | - | <a href="/demo" data-key="s" aria-keyshortcuts="g s">summary</a> | 58 | + | <a href="/demo" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 59 | + | </a> | |||
| 39 | </li> | 60 | </li> | ||
| 40 | <li> | 61 | <li> | ||
| 41 | - | <a href="/demo/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 62 | + | <a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 63 | + | </a> | |||
| 42 | </li> | 64 | </li> | ||
| 43 | <li> | 65 | <li> | ||
| 44 | - | <a href="/demo/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 66 | + | <a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 67 | + | </a> | |||
| 45 | </li> | 68 | </li> | ||
| 46 | <li> | 69 | <li> | ||
| 47 | - | <a href="/demo/refs" data-key="r" aria-keyshortcuts="g r" aria-current="page">refs</a> | 70 | + | <a href="/demo/refs" data-key="r" aria-keyshortcuts="r" aria-current="page">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 71 | + | </a> | |||
| 48 | </li> | 72 | </li> | ||
| 49 | <li> | 73 | <li> | ||
| 50 | - | <a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 74 | + | <a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 75 | + | </a> | |||
| 51 | </li> | 76 | </li> | ||
| 52 | </ul> | 77 | </ul> | ||
| 53 | </nav> | 78 | </nav> | ||
| 54 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 55 | - | </button> | |||
| 56 | </header> | 79 | </header> | ||
| 57 | <div class="work has-rail"> | 80 | <div class="work has-rail"> | ||
| 58 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 81 | + | <main id="main" tabindex="-1"> |
| 59 | <div class="filter"> | 82 | <div class="filter"> | ||
| 60 | - | <label for="ref-filter">filter></label> | 83 | + | <label for="ref-filter" class="sr">filter</label> |
| 61 | - | <input id="ref-filter" type="search" data-filter placeholder="branch or tag ( / )" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#ref-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | 84 | + | <input id="ref-filter" type="search" data-filter placeholder="Filter branches and tags…" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#ref-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); |
| 62 | let first = q('#ref-rows > tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#ref-rows > tr')"> | 85 | let first = q('#ref-rows > tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#ref-rows > tr')"> | ||
| 63 | </div> | 86 | </div> | ||
| 64 | <table class="list refs" aria-label="Branches and tags"> | 87 | <table class="list refs" aria-label="Branches and tags"> | ||
| @@ -80,7 +103,7 @@ | |||||
| 80 | <td class="c-kind mute">branch</td> | 103 | <td class="c-kind mute">branch</td> | ||
| 81 | <td class="c-name"> | 104 | <td class="c-name"> | ||
| 82 | <a href="/demo/tree/main">main</a> | 105 | <a href="/demo/tree/main">main</a> | ||
| 83 | - | <span class="tagged">*</span> | 106 | + | <span class="pill">default</span> |
| 84 | </td> | 107 | </td> | ||
| 85 | <td class="c-sha"> | 108 | <td class="c-sha"> | ||
| 86 | <a href="/demo/commit/SHA40">SHA7</a> | 109 | <a href="/demo/commit/SHA40">SHA7</a> | ||
| @@ -139,10 +162,8 @@ | |||||
| 139 | </table> | 162 | </table> | ||
| 140 | </main> | 163 | </main> | ||
| 141 | <aside class="rail" aria-label="Context"> | 164 | <aside class="rail" aria-label="Context"> | ||
| 142 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 143 | - | <summary>refs · legend</summary> | |||
| 144 | <section aria-labelledby="r-refs"> | 165 | <section aria-labelledby="r-refs"> | ||
| 145 | - | <h2 id="r-refs">refs</h2> | 166 | + | <h2 id="r-refs">Overview</h2> |
| 146 | <dl class="kv" id="refs-kv"> | 167 | <dl class="kv" id="refs-kv"> | ||
| 147 | <dt>default</dt> | 168 | <dt>default</dt> | ||
| 148 | <dd>main</dd> | 169 | <dd>main</dd> | ||
| @@ -154,80 +175,78 @@ | |||||
| 154 | <dd>v0.2.0</dd> | 175 | <dd>v0.2.0</dd> | ||
| 155 | </dl> | 176 | </dl> | ||
| 156 | </section> | 177 | </section> | ||
| 157 | - | <section aria-labelledby="r-legend"> | |||
| 158 | - | <h2 id="r-legend">legend</h2> | |||
| 159 | - | <dl class="kv"> | |||
| 160 | - | <dt>*</dt> | |||
| 161 | - | <dd>default branch</dd> | |||
| 162 | - | </dl> | |||
| 163 | - | </section> | |||
| 164 | - | </details> | |||
| 165 | </aside> | 178 | </aside> | ||
| 166 | </div> | 179 | </div> | ||
| 167 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 180 | + | <footer class="foot"> |
| 181 | + | <span>ermite devel</span> | |||
| 182 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 183 | + | <span class="foot-keys"> | |||
| 184 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 185 | + | </footer> | |||
| 186 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 187 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 188 | + | </span> | |||
| 189 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 190 | + | </span> | |||
| 191 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 192 | + | </span> | |||
| 193 | + | <span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&view=refs" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |||
| 194 | + | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |||
| 195 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |||
| 196 | + | gap -> ermiteLive.reload()"> | |||
| 197 | + | </span> | |||
| 198 | + | </div> | |||
| 199 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 168 | <div class="panel-head"> | 200 | <div class="panel-head"> | ||
| 169 | - | <span>keys</span> | 201 | + | <h2>Keyboard shortcuts</h2> |
| 170 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 202 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 203 | + | <kbd>esc</kbd> close</button> | |||
| 171 | </div> | 204 | </div> | ||
| 172 | <dl class="keys"> | 205 | <dl class="keys"> | ||
| 173 | <div> | 206 | <div> | ||
| 174 | <dt> | 207 | <dt> | ||
| 175 | - | <kbd>j</kbd> | 208 | + | <kbd>ctrl k</kbd> |
| 176 | - | <kbd>k</kbd> | 209 | + | <kbd>:</kbd> |
| 177 | </dt> | 210 | </dt> | ||
| 178 | - | <dd>move cursor down / up</dd> | 211 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 179 | </div> | 212 | </div> | ||
| 180 | <div> | 213 | <div> | ||
| 181 | <dt> | 214 | <dt> | ||
| 182 | - | <kbd>enter</kbd> | 215 | + | <kbd>?</kbd> |
| 183 | </dt> | 216 | </dt> | ||
| 184 | - | <dd>open row under cursor</dd> | 217 | + | <dd>this list</dd> |
| 185 | </div> | 218 | </div> | ||
| 186 | <div> | 219 | <div> | ||
| 187 | <dt> | 220 | <dt> | ||
| 188 | - | <kbd>g</kbd> | 221 | + | <kbd>j</kbd> |
| 189 | - | <kbd>g</kbd> / <kbd>G</kbd> | 222 | + | <kbd>k</kbd> |
| 190 | </dt> | 223 | </dt> | ||
| 191 | - | <dd>first / last row</dd> | 224 | + | <dd>move the row cursor down / up</dd> |
| 192 | </div> | 225 | </div> | ||
| 193 | <div> | 226 | <div> | ||
| 194 | <dt> | 227 | <dt> | ||
| 195 | - | <kbd>/</kbd> | 228 | + | <kbd>enter</kbd> |
| 196 | </dt> | 229 | </dt> | ||
| 197 | - | <dd>focus the filter</dd> | 230 | + | <dd>open the row under the cursor</dd> |
| 198 | </div> | 231 | </div> | ||
| 199 | <div> | 232 | <div> | ||
| 200 | <dt> | 233 | <dt> | ||
| 201 | - | <kbd>g h</kbd> | 234 | + | <kbd>g g</kbd> |
| 202 | - | <kbd>g t</kbd> | 235 | + | <kbd>G</kbd> |
| 203 | </dt> | 236 | </dt> | ||
| 204 | - | <dd>go home / tree</dd> | 237 | + | <dd>first / last row</dd> |
| 205 | </div> | 238 | </div> | ||
| 206 | <div> | 239 | <div> | ||
| 207 | <dt> | 240 | <dt> | ||
| 208 | - | <kbd>g c</kbd> | 241 | + | <kbd>/</kbd> |
| 209 | - | <kbd>g r</kbd> | |||
| 210 | </dt> | 242 | </dt> | ||
| 211 | - | <dd>go commits / refs</dd> | 243 | + | <dd>filter the list</dd> |
| 212 | </div> | 244 | </div> | ||
| 213 | <div> | 245 | <div> | ||
| 214 | <dt> | 246 | <dt> | ||
| 215 | <kbd>b</kbd> | 247 | <kbd>b</kbd> | ||
| 216 | </dt> | 248 | </dt> | ||
| 217 | - | <dd>switch ref (branches, tags)</dd> | 249 | + | <dd>switch branch or tag</dd> |
| 218 | - | </div> | |||
| 219 | - | <div> | |||
| 220 | - | <dt> | |||
| 221 | - | <kbd>:</kbd> | |||
| 222 | - | <kbd>f</kbd> | |||
| 223 | - | </dt> | |||
| 224 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 225 | - | </div> | |||
| 226 | - | <div> | |||
| 227 | - | <dt> | |||
| 228 | - | <kbd>y</kbd> | |||
| 229 | - | </dt> | |||
| 230 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 231 | </div> | 250 | </div> | ||
| 232 | <div> | 251 | <div> | ||
| 233 | <dt> | 252 | <dt> | ||
| @@ -235,15 +254,21 @@ | |||||
| 235 | <kbd>t</kbd> | 254 | <kbd>t</kbd> | ||
| 236 | <kbd>c</kbd> | 255 | <kbd>c</kbd> | ||
| 237 | <kbd>r</kbd> | 256 | <kbd>r</kbd> | ||
| 257 | + | <kbd>m</kbd> | |||
| 238 | </dt> | 258 | </dt> | ||
| 239 | - | <dd>repo sections (when shown)</dd> | 259 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 240 | </div> | 260 | </div> | ||
| 241 | <div> | 261 | <div> | ||
| 242 | <dt> | 262 | <dt> | ||
| 243 | - | <kbd>m</kbd> | 263 | + | <kbd>g h</kbd> |
| 244 | - | <kbd>g m</kbd> | |||
| 245 | </dt> | 264 | </dt> | ||
| 246 | - | <dd>merge requests (when shown)</dd> | 265 | + | <dd>home</dd> |
| 266 | + | </div> | |||
| 267 | + | <div> | |||
| 268 | + | <dt> | |||
| 269 | + | <kbd>y</kbd> | |||
| 270 | + | </dt> | |||
| 271 | + | <dd>copy a permalink pinned to the commit</dd> | |||
| 247 | </div> | 272 | </div> | ||
| 248 | <div> | 273 | <div> | ||
| 249 | <dt> | 274 | <dt> | ||
| @@ -263,19 +288,21 @@ | |||||
| 263 | <dt> | 288 | <dt> | ||
| 264 | <kbd>esc</kbd> | 289 | <kbd>esc</kbd> | ||
| 265 | </dt> | 290 | </dt> | ||
| 266 | - | <dd>close panel</dd> | 291 | + | <dd>close a dialog, leave a field</dd> |
| 267 | </div> | 292 | </div> | ||
| 268 | </dl> | 293 | </dl> | ||
| 269 | </dialog> | 294 | </dialog> | ||
| 270 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 295 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 271 | <div class="panel-head"> | 296 | <div class="panel-head"> | ||
| 272 | - | <label for="refs-filter">ref></label> | 297 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 273 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 298 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 274 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 299 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 300 | + | <kbd>esc</kbd> | |||
| 301 | + | </button> | |||
| 275 | </div> | 302 | </div> | ||
| 276 | <div class="panel-cols" data-picklist> | 303 | <div class="panel-cols" data-picklist> | ||
| 277 | <section aria-label="Branches"> | 304 | <section aria-label="Branches"> | ||
| 278 | - | <h2>branches <span class="mute">2</span> | 305 | + | <h2>Branches <span class="count">2</span> |
| 279 | </h2> | 306 | </h2> | ||
| 280 | <ul> | 307 | <ul> | ||
| 281 | <li> | 308 | <li> | ||
| @@ -289,7 +316,7 @@ | |||||
| 289 | </ul> | 316 | </ul> | ||
| 290 | </section> | 317 | </section> | ||
| 291 | <section aria-label="Tags"> | 318 | <section aria-label="Tags"> | ||
| 292 | - | <h2>tags <span class="mute">2</span> | 319 | + | <h2>Tags <span class="count">2</span> |
| 293 | </h2> | 320 | </h2> | ||
| 294 | <ul> | 321 | <ul> | ||
| 295 | <li> | 322 | <li> | ||
| @@ -304,117 +331,136 @@ | |||||
| 304 | </section> | 331 | </section> | ||
| 305 | </div> | 332 | </div> | ||
| 306 | </dialog> | 333 | </dialog> | ||
| 307 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 334 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 308 | <div class="panel-head"> | 335 | <div class="panel-head"> | ||
| 309 | - | <label for="cmd-q" class="hi">:</label> | 336 | + | <label for="cmd-q" class="sr">go to</label> |
| 310 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 337 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 311 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 338 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 312 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 339 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 313 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 340 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 314 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 341 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 315 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 342 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 316 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 343 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 317 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 344 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 345 | + | <kbd>esc</kbd> | |||
| 346 | + | </button> | |||
| 318 | </div> | 347 | </div> | ||
| 319 | - | <ul id="cmd-list" class="cmd-list"> | 348 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 320 | <li data-k="go home" data-cursor> | 349 | <li data-k="go home" data-cursor> | ||
| 321 | <a href="/" tabindex="-1"> | 350 | <a href="/" tabindex="-1"> | ||
| 322 | - | <span class="k">go</span>home</a> | 351 | + | <span class="lbl">home</span> |
| 352 | + | <span class="k">go</span> | |||
| 353 | + | </a> | |||
| 323 | </li> | 354 | </li> | ||
| 324 | <li data-k="section summary"> | 355 | <li data-k="section summary"> | ||
| 325 | <a href="/demo" tabindex="-1"> | 356 | <a href="/demo" tabindex="-1"> | ||
| 326 | - | <span class="k">section</span>summary</a> | 357 | + | <span class="lbl">summary</span> |
| 358 | + | <span class="k">section</span> | |||
| 359 | + | </a> | |||
| 327 | </li> | 360 | </li> | ||
| 328 | - | <li data-k="section tree"> | 361 | + | <li data-k="section files"> |
| 329 | <a href="/demo/tree/main" tabindex="-1"> | 362 | <a href="/demo/tree/main" tabindex="-1"> | ||
| 330 | - | <span class="k">section</span>tree</a> | 363 | + | <span class="lbl">files</span> |
| 364 | + | <span class="k">section</span> | |||
| 365 | + | </a> | |||
| 331 | </li> | 366 | </li> | ||
| 332 | <li data-k="section commits"> | 367 | <li data-k="section commits"> | ||
| 333 | <a href="/demo/commits/main" tabindex="-1"> | 368 | <a href="/demo/commits/main" tabindex="-1"> | ||
| 334 | - | <span class="k">section</span>commits</a> | 369 | + | <span class="lbl">commits</span> |
| 370 | + | <span class="k">section</span> | |||
| 371 | + | </a> | |||
| 335 | </li> | 372 | </li> | ||
| 336 | - | <li data-k="section refs"> | 373 | + | <li data-k="section branches & tags"> |
| 337 | <a href="/demo/refs" tabindex="-1"> | 374 | <a href="/demo/refs" tabindex="-1"> | ||
| 338 | - | <span class="k">section</span>refs</a> | 375 | + | <span class="lbl">branches & tags</span> |
| 376 | + | <span class="k">section</span> | |||
| 377 | + | </a> | |||
| 339 | </li> | 378 | </li> | ||
| 340 | <li data-k="section merge requests"> | 379 | <li data-k="section merge requests"> | ||
| 341 | <a href="/demo/merge-requests" tabindex="-1"> | 380 | <a href="/demo/merge-requests" tabindex="-1"> | ||
| 342 | - | <span class="k">section</span>merge requests</a> | 381 | + | <span class="lbl">merge requests</span> |
| 382 | + | <span class="k">section</span> | |||
| 383 | + | </a> | |||
| 343 | </li> | 384 | </li> | ||
| 344 | <li data-k="branch feature/x"> | 385 | <li data-k="branch feature/x"> | ||
| 345 | <a href="/demo/tree/feature/x" tabindex="-1"> | 386 | <a href="/demo/tree/feature/x" tabindex="-1"> | ||
| 346 | - | <span class="k">branch</span>feature/x</a> | 387 | + | <span class="lbl">feature/x</span> |
| 388 | + | <span class="k">branch</span> | |||
| 389 | + | </a> | |||
| 347 | </li> | 390 | </li> | ||
| 348 | <li data-k="branch main"> | 391 | <li data-k="branch main"> | ||
| 349 | <a href="/demo/tree/main" tabindex="-1"> | 392 | <a href="/demo/tree/main" tabindex="-1"> | ||
| 350 | - | <span class="k">branch</span>main</a> | 393 | + | <span class="lbl">main</span> |
| 394 | + | <span class="k">branch</span> | |||
| 395 | + | </a> | |||
| 351 | </li> | 396 | </li> | ||
| 352 | <li data-k="tag v0.1.0"> | 397 | <li data-k="tag v0.1.0"> | ||
| 353 | <a href="/demo/tree/v0.1.0" tabindex="-1"> | 398 | <a href="/demo/tree/v0.1.0" tabindex="-1"> | ||
| 354 | - | <span class="k">tag</span>v0.1.0</a> | 399 | + | <span class="lbl">v0.1.0</span> |
| 400 | + | <span class="k">tag</span> | |||
| 401 | + | </a> | |||
| 355 | </li> | 402 | </li> | ||
| 356 | <li data-k="tag v0.2.0"> | 403 | <li data-k="tag v0.2.0"> | ||
| 357 | <a href="/demo/tree/v0.2.0" tabindex="-1"> | 404 | <a href="/demo/tree/v0.2.0" tabindex="-1"> | ||
| 358 | - | <span class="k">tag</span>v0.2.0</a> | 405 | + | <span class="lbl">v0.2.0</span> |
| 406 | + | <span class="k">tag</span> | |||
| 407 | + | </a> | |||
| 359 | </li> | 408 | </li> | ||
| 360 | <li data-k="repo quiet"> | 409 | <li data-k="repo quiet"> | ||
| 361 | <a href="/quiet" tabindex="-1"> | 410 | <a href="/quiet" tabindex="-1"> | ||
| 362 | - | <span class="k">repo</span>quiet</a> | 411 | + | <span class="lbl">quiet</span> |
| 412 | + | <span class="k">repo</span> | |||
| 413 | + | </a> | |||
| 363 | </li> | 414 | </li> | ||
| 364 | </ul> | 415 | </ul> | ||
| 416 | + | <p class="panel-foot"> | |||
| 417 | + | <span> | |||
| 418 | + | <kbd>↑</kbd> | |||
| 419 | + | <kbd>↓</kbd> move</span> | |||
| 420 | + | <span> | |||
| 421 | + | <kbd>enter</kbd> open</span> | |||
| 422 | + | <span> | |||
| 423 | + | <kbd>esc</kbd> close</span> | |||
| 424 | + | </p> | |||
| 365 | </dialog> | 425 | </dialog> | ||
| 366 | - | <footer class="bar status" aria-label="Status"> | 426 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 367 | - | <span class="mode">refs</span> | |||
| 368 | - | <span class="st-ctx">2 branches · 2 tags</span> | |||
| 369 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 370 | - | <span class="hi">:</span> go to…</button> | |||
| 371 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 372 | - | </span> | |||
| 373 | - | <span class="st-hints">j/k move · enter open · / filter · ? help</span> | |||
| 374 | - | <span class="st-src"> | |||
| 375 | - | <span class="st-ver">ermite devel · </span> | |||
| 376 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 377 | - | </span> | |||
| 378 | - | <span class="st-live" data-state="offline" role="status" hx-sse:connect="/-/events?topic=repo%3Ademo&view=refs" hx-swap="none" :text="data.state" :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |||
| 379 | - | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |||
| 380 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |||
| 381 | - | gap -> ermiteLive.reload()">offline</span> | |||
| 382 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/4</span> | |||
| 383 | - | </footer> | |||
| 384 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 385 | if (q('dialog[open]').count) return; | 427 | if (q('dialog[open]').count) return; | ||
| 386 | let rows = q('[data-rows] > :not([hidden])').arr(); | 428 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 387 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 429 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 388 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 430 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 389 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 431 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 390 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 432 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 433 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 434 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 391 | })(1); | 435 | })(1); | ||
| 392 | - | k-k from:body -> ((d) => { | 436 | + | k-k from:body -> (async (d) => { |
| 393 | if (q('dialog[open]').count) return; | 437 | if (q('dialog[open]').count) return; | ||
| 394 | let rows = q('[data-rows] > :not([hidden])').arr(); | 438 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 395 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 439 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 396 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 440 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 397 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 441 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 398 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 442 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 443 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 444 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 399 | })(-1); | 445 | })(-1); | ||
| 400 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 446 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 401 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 447 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 402 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 448 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 403 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 449 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 404 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 450 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 405 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 451 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 406 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 452 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 407 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 453 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 408 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 454 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 409 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 455 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 410 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 456 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 411 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 457 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 412 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 458 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 413 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 459 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 414 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 460 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 415 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 461 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 416 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 462 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 417 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 463 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 418 | k-next from:body -> { | 464 | k-next from:body -> { | ||
| 419 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 465 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 420 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 466 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/summary.html +184 -127
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>demo · example.test</title> | 7 | <title>demo · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -29,34 +33,54 @@ | |||||
| 29 | </li> | 33 | </li> | ||
| 30 | </ol> | 34 | </ol> | ||
| 31 | </nav> | 35 | </nav> | ||
| 32 | - | <nav class="secs" aria-label="Sections"> | 36 | + | <div class="hdr-acts"> |
| 37 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 38 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 39 | + | </span> | |||
| 40 | + | <span class="ref-name">main</span> | |||
| 41 | + | <span class="sha">SHA7</span> | |||
| 42 | + | </button> | |||
| 43 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 44 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="search-label">Go to…</span> | |||
| 47 | + | <kbd>ctrl k</kbd> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 50 | + | </div> | |||
| 51 | + | </div> | |||
| 52 | + | <nav class="tabs" aria-label="Sections"> | |||
| 33 | <ul> | 53 | <ul> | ||
| 34 | <li> | 54 | <li> | ||
| 35 | - | <a href="/demo" data-key="s" aria-keyshortcuts="g s" aria-current="page">summary</a> | 55 | + | <a href="/demo" data-key="s" aria-keyshortcuts="s" aria-current="page">Summary<kbd aria-hidden="true">s</kbd> |
| 56 | + | </a> | |||
| 36 | </li> | 57 | </li> | ||
| 37 | <li> | 58 | <li> | ||
| 38 | - | <a href="/demo/tree/main" data-key="t" aria-keyshortcuts="g t">tree</a> | 59 | + | <a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd> |
| 60 | + | </a> | |||
| 39 | </li> | 61 | </li> | ||
| 40 | <li> | 62 | <li> | ||
| 41 | - | <a href="/demo/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 63 | + | <a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 64 | + | </a> | |||
| 42 | </li> | 65 | </li> | ||
| 43 | <li> | 66 | <li> | ||
| 44 | - | <a href="/demo/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 67 | + | <a href="/demo/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 68 | + | </a> | |||
| 45 | </li> | 69 | </li> | ||
| 46 | <li> | 70 | <li> | ||
| 47 | - | <a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 71 | + | <a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 72 | + | </a> | |||
| 48 | </li> | 73 | </li> | ||
| 49 | </ul> | 74 | </ul> | ||
| 50 | </nav> | 75 | </nav> | ||
| 51 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 52 | - | </button> | |||
| 53 | </header> | 76 | </header> | ||
| 54 | - | <div class="work has-rail read"> | 77 | + | <div class="work has-rail"> |
| 55 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 78 | + | <main id="main" tabindex="-1"> |
| 56 | <p class="lede">A demo repository</p> | 79 | <p class="lede">A demo repository</p> | ||
| 57 | - | <h2 class="rule-h"> | 80 | + | <section class="card readme" aria-labelledby="readme-h"> |
| 58 | - | <span>README.md</span> | 81 | + | <h2 class="card-h" id="readme-h"> |
| 59 | - | </h2> | 82 | + | <span class="ico i-file" aria-hidden="true"> |
| 83 | + | </span> README.md</h2> | |||
| 60 | <article class="prose" aria-label="README.md"> | 84 | <article class="prose" aria-label="README.md"> | ||
| 61 | <h1 id="md-demo">Demo</h1> | 85 | <h1 id="md-demo">Demo</h1> | ||
| 62 | <p>A small <em>demo</em> with a <a href="/demo/blob/main/docs/guide.md">guide</a> and <a href="https://example.org/x" rel="nofollow noopener noreferrer">the site</a>.</p> | 86 | <p>A small <em>demo</em> with a <a href="/demo/blob/main/docs/guide.md">guide</a> and <a href="https://example.org/x" rel="nofollow noopener noreferrer">the site</a>.</p> | ||
| @@ -81,22 +105,21 @@ | |||||
| 81 | </tbody> | 105 | </tbody> | ||
| 82 | </table> | 106 | </table> | ||
| 83 | </article> | 107 | </article> | ||
| 108 | + | </section> | |||
| 84 | </main> | 109 | </main> | ||
| 85 | <aside class="rail" aria-label="Context"> | 110 | <aside class="rail" aria-label="Context"> | ||
| 86 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | |||
| 87 | - | <summary>clone · facts · recent commits · outline</summary> | |||
| 88 | <section aria-labelledby="r-clone"> | 111 | <section aria-labelledby="r-clone"> | ||
| 89 | - | <h2 id="r-clone">clone</h2> | 112 | + | <h2 id="r-clone">Clone</h2> |
| 90 | <div class="clone" id="clone-demo"> | 113 | <div class="clone" id="clone-demo"> | ||
| 91 | <div class="clone-line"> | 114 | <div class="clone-line"> | ||
| 92 | <span class="mute" aria-hidden="true">$ </span> | 115 | <span class="mute" aria-hidden="true">$ </span> | ||
| 93 | <code>git clone http://example.test/demo.git</code> | 116 | <code>git clone http://example.test/demo.git</code> | ||
| 94 | - | <button type="button" class="copy" data-label="[copy]" aria-label="copy: git clone http://example.test/demo.git" hx-on:click="navigator.clipboard?.writeText(closest('.clone-line').querySelector('code').textContent); this.textContent = '[copied]'; await timeout('1500ms'); this.textContent = this.dataset.label">[copy]</button> | 117 | + | <button type="button" class="btn copy" data-label="Copy" aria-label="copy: git clone http://example.test/demo.git" hx-on:click="navigator.clipboard?.writeText(closest('.clone-line').querySelector('code').textContent); this.textContent = 'Copied'; await timeout('1500ms'); this.textContent = this.dataset.label">Copy</button> |
| 95 | </div> | 118 | </div> | ||
| 96 | </div> | 119 | </div> | ||
| 97 | </section> | 120 | </section> | ||
| 98 | <section aria-labelledby="r-facts"> | 121 | <section aria-labelledby="r-facts"> | ||
| 99 | - | <h2 id="r-facts">facts</h2> | 122 | + | <h2 id="r-facts">About</h2> |
| 100 | <dl class="kv"> | 123 | <dl class="kv"> | ||
| 101 | <dt>last push</dt> | 124 | <dt>last push</dt> | ||
| 102 | <dd id="last-push-demo"> | 125 | <dd id="last-push-demo"> | ||
| @@ -113,7 +136,7 @@ | |||||
| 113 | </dl> | 136 | </dl> | ||
| 114 | </section> | 137 | </section> | ||
| 115 | <section aria-labelledby="r-recent"> | 138 | <section aria-labelledby="r-recent"> | ||
| 116 | - | <h2 id="r-recent">recent commits <a href="/demo/commits/main">all</a> | 139 | + | <h2 id="r-recent">Recent commits <a href="/demo/commits/main">View all</a> |
| 117 | </h2> | 140 | </h2> | ||
| 118 | <table class="list commits" aria-label="Commits"> | 141 | <table class="list commits" aria-label="Commits"> | ||
| 119 | <tbody data-rows> | 142 | <tbody data-rows> | ||
| @@ -123,8 +146,9 @@ | |||||
| 123 | <td class="c-sha mute">SHA7</td> | 146 | <td class="c-sha mute">SHA7</td> | ||
| 124 | <td class="c-subject"> | 147 | <td class="c-subject"> | ||
| 125 | <a href="/demo/commit/SHA40">add main.go</a> | 148 | <a href="/demo/commit/SHA40">add main.go</a> | ||
| 126 | - | <span class="tagged">[main]</span> | 149 | + | <span class="pill">main</span> |
| 127 | - | <span class="tagged">[tag: v0.2.0]</span> | 150 | + | <span class="pill tag"> |
| 151 | + | <span class="sr">tag </span>v0.2.0</span> | |||
| 128 | </td> | 152 | </td> | ||
| 129 | <td class="c-who dim">T</td> | 153 | <td class="c-who dim">T</td> | ||
| 130 | <td class="c-when nowrap dim"> | 154 | <td class="c-when nowrap dim"> | ||
| @@ -137,7 +161,8 @@ | |||||
| 137 | <td class="c-sha mute">SHA7</td> | 161 | <td class="c-sha mute">SHA7</td> | ||
| 138 | <td class="c-subject"> | 162 | <td class="c-subject"> | ||
| 139 | <a href="/demo/commit/SHA40">first commit</a> | 163 | <a href="/demo/commit/SHA40">first commit</a> | ||
| 140 | - | <span class="tagged">[tag: v0.1.0]</span> | 164 | + | <span class="pill tag"> |
| 165 | + | <span class="sr">tag </span>v0.1.0</span> | |||
| 141 | </td> | 166 | </td> | ||
| 142 | <td class="c-who dim">T</td> | 167 | <td class="c-who dim">T</td> | ||
| 143 | <td class="c-when nowrap dim"> | 168 | <td class="c-when nowrap dim"> | ||
| @@ -148,80 +173,85 @@ | |||||
| 148 | </table> | 173 | </table> | ||
| 149 | </section> | 174 | </section> | ||
| 150 | <section aria-labelledby="r-outline"> | 175 | <section aria-labelledby="r-outline"> | ||
| 151 | - | <h2 id="r-outline">README.md</h2> | 176 | + | <h2 id="r-outline">On this page</h2> |
| 152 | <nav class="outline" aria-label="Outline"> | 177 | <nav class="outline" aria-label="Outline"> | ||
| 153 | <a class="l1" href="#md-demo">Demo</a> | 178 | <a class="l1" href="#md-demo">Demo</a> | ||
| 154 | <a class="l2" href="#md-install">Install</a> | 179 | <a class="l2" href="#md-install">Install</a> | ||
| 155 | <a class="l2" href="#md-usage">Usage</a> | 180 | <a class="l2" href="#md-usage">Usage</a> | ||
| 156 | </nav> | 181 | </nav> | ||
| 157 | </section> | 182 | </section> | ||
| 158 | - | </details> | |||
| 159 | </aside> | 183 | </aside> | ||
| 160 | </div> | 184 | </div> | ||
| 161 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 185 | + | <footer class="foot"> |
| 186 | + | <span>ermite devel</span> | |||
| 187 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 188 | + | <span class="foot-keys"> | |||
| 189 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 190 | + | </footer> | |||
| 191 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 192 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 193 | + | </span> | |||
| 194 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 195 | + | </span> | |||
| 196 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 197 | + | </span> | |||
| 198 | + | <span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&view=summary" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |||
| 199 | + | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |||
| 200 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |||
| 201 | + | gap -> ermiteLive.reload()"> | |||
| 202 | + | </span> | |||
| 203 | + | </div> | |||
| 204 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 162 | <div class="panel-head"> | 205 | <div class="panel-head"> | ||
| 163 | - | <span>keys</span> | 206 | + | <h2>Keyboard shortcuts</h2> |
| 164 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 207 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 208 | + | <kbd>esc</kbd> close</button> | |||
| 165 | </div> | 209 | </div> | ||
| 166 | <dl class="keys"> | 210 | <dl class="keys"> | ||
| 167 | <div> | 211 | <div> | ||
| 168 | <dt> | 212 | <dt> | ||
| 169 | - | <kbd>j</kbd> | 213 | + | <kbd>ctrl k</kbd> |
| 170 | - | <kbd>k</kbd> | 214 | + | <kbd>:</kbd> |
| 171 | </dt> | 215 | </dt> | ||
| 172 | - | <dd>move cursor down / up</dd> | 216 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 173 | </div> | 217 | </div> | ||
| 174 | <div> | 218 | <div> | ||
| 175 | <dt> | 219 | <dt> | ||
| 176 | - | <kbd>enter</kbd> | 220 | + | <kbd>?</kbd> |
| 177 | </dt> | 221 | </dt> | ||
| 178 | - | <dd>open row under cursor</dd> | 222 | + | <dd>this list</dd> |
| 179 | </div> | 223 | </div> | ||
| 180 | <div> | 224 | <div> | ||
| 181 | <dt> | 225 | <dt> | ||
| 182 | - | <kbd>g</kbd> | 226 | + | <kbd>j</kbd> |
| 183 | - | <kbd>g</kbd> / <kbd>G</kbd> | 227 | + | <kbd>k</kbd> |
| 184 | </dt> | 228 | </dt> | ||
| 185 | - | <dd>first / last row</dd> | 229 | + | <dd>move the row cursor down / up</dd> |
| 186 | </div> | 230 | </div> | ||
| 187 | <div> | 231 | <div> | ||
| 188 | <dt> | 232 | <dt> | ||
| 189 | - | <kbd>/</kbd> | 233 | + | <kbd>enter</kbd> |
| 190 | </dt> | 234 | </dt> | ||
| 191 | - | <dd>focus the filter</dd> | 235 | + | <dd>open the row under the cursor</dd> |
| 192 | </div> | 236 | </div> | ||
| 193 | <div> | 237 | <div> | ||
| 194 | <dt> | 238 | <dt> | ||
| 195 | - | <kbd>g h</kbd> | 239 | + | <kbd>g g</kbd> |
| 196 | - | <kbd>g t</kbd> | 240 | + | <kbd>G</kbd> |
| 197 | </dt> | 241 | </dt> | ||
| 198 | - | <dd>go home / tree</dd> | 242 | + | <dd>first / last row</dd> |
| 199 | </div> | 243 | </div> | ||
| 200 | <div> | 244 | <div> | ||
| 201 | <dt> | 245 | <dt> | ||
| 202 | - | <kbd>g c</kbd> | 246 | + | <kbd>/</kbd> |
| 203 | - | <kbd>g r</kbd> | |||
| 204 | </dt> | 247 | </dt> | ||
| 205 | - | <dd>go commits / refs</dd> | 248 | + | <dd>filter the list</dd> |
| 206 | </div> | 249 | </div> | ||
| 207 | <div> | 250 | <div> | ||
| 208 | <dt> | 251 | <dt> | ||
| 209 | <kbd>b</kbd> | 252 | <kbd>b</kbd> | ||
| 210 | </dt> | 253 | </dt> | ||
| 211 | - | <dd>switch ref (branches, tags)</dd> | 254 | + | <dd>switch branch or tag</dd> |
| 212 | - | </div> | |||
| 213 | - | <div> | |||
| 214 | - | <dt> | |||
| 215 | - | <kbd>:</kbd> | |||
| 216 | - | <kbd>f</kbd> | |||
| 217 | - | </dt> | |||
| 218 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 219 | - | </div> | |||
| 220 | - | <div> | |||
| 221 | - | <dt> | |||
| 222 | - | <kbd>y</kbd> | |||
| 223 | - | </dt> | |||
| 224 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 225 | </div> | 255 | </div> | ||
| 226 | <div> | 256 | <div> | ||
| 227 | <dt> | 257 | <dt> | ||
| @@ -229,15 +259,21 @@ | |||||
| 229 | <kbd>t</kbd> | 259 | <kbd>t</kbd> | ||
| 230 | <kbd>c</kbd> | 260 | <kbd>c</kbd> | ||
| 231 | <kbd>r</kbd> | 261 | <kbd>r</kbd> | ||
| 262 | + | <kbd>m</kbd> | |||
| 232 | </dt> | 263 | </dt> | ||
| 233 | - | <dd>repo sections (when shown)</dd> | 264 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 234 | </div> | 265 | </div> | ||
| 235 | <div> | 266 | <div> | ||
| 236 | <dt> | 267 | <dt> | ||
| 237 | - | <kbd>m</kbd> | 268 | + | <kbd>g h</kbd> |
| 238 | - | <kbd>g m</kbd> | |||
| 239 | </dt> | 269 | </dt> | ||
| 240 | - | <dd>merge requests (when shown)</dd> | 270 | + | <dd>home</dd> |
| 271 | + | </div> | |||
| 272 | + | <div> | |||
| 273 | + | <dt> | |||
| 274 | + | <kbd>y</kbd> | |||
| 275 | + | </dt> | |||
| 276 | + | <dd>copy a permalink pinned to the commit</dd> | |||
| 241 | </div> | 277 | </div> | ||
| 242 | <div> | 278 | <div> | ||
| 243 | <dt> | 279 | <dt> | ||
| @@ -257,19 +293,21 @@ | |||||
| 257 | <dt> | 293 | <dt> | ||
| 258 | <kbd>esc</kbd> | 294 | <kbd>esc</kbd> | ||
| 259 | </dt> | 295 | </dt> | ||
| 260 | - | <dd>close panel</dd> | 296 | + | <dd>close a dialog, leave a field</dd> |
| 261 | </div> | 297 | </div> | ||
| 262 | </dl> | 298 | </dl> | ||
| 263 | </dialog> | 299 | </dialog> | ||
| 264 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 300 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 265 | <div class="panel-head"> | 301 | <div class="panel-head"> | ||
| 266 | - | <label for="refs-filter">ref></label> | 302 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 267 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 303 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 268 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 304 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 305 | + | <kbd>esc</kbd> | |||
| 306 | + | </button> | |||
| 269 | </div> | 307 | </div> | ||
| 270 | <div class="panel-cols" data-picklist> | 308 | <div class="panel-cols" data-picklist> | ||
| 271 | <section aria-label="Branches"> | 309 | <section aria-label="Branches"> | ||
| 272 | - | <h2>branches <span class="mute">2</span> | 310 | + | <h2>Branches <span class="count">2</span> |
| 273 | </h2> | 311 | </h2> | ||
| 274 | <ul> | 312 | <ul> | ||
| 275 | <li> | 313 | <li> | ||
| @@ -283,7 +321,7 @@ | |||||
| 283 | </ul> | 321 | </ul> | ||
| 284 | </section> | 322 | </section> | ||
| 285 | <section aria-label="Tags"> | 323 | <section aria-label="Tags"> | ||
| 286 | - | <h2>tags <span class="mute">2</span> | 324 | + | <h2>Tags <span class="count">2</span> |
| 287 | </h2> | 325 | </h2> | ||
| 288 | <ul> | 326 | <ul> | ||
| 289 | <li> | 327 | <li> | ||
| @@ -298,117 +336,136 @@ | |||||
| 298 | </section> | 336 | </section> | ||
| 299 | </div> | 337 | </div> | ||
| 300 | </dialog> | 338 | </dialog> | ||
| 301 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 339 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 302 | <div class="panel-head"> | 340 | <div class="panel-head"> | ||
| 303 | - | <label for="cmd-q" class="hi">:</label> | 341 | + | <label for="cmd-q" class="sr">go to</label> |
| 304 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 342 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 305 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 343 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 306 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 344 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 307 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 345 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 308 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 346 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 309 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 347 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 310 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 348 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 311 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 349 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 350 | + | <kbd>esc</kbd> | |||
| 351 | + | </button> | |||
| 312 | </div> | 352 | </div> | ||
| 313 | - | <ul id="cmd-list" class="cmd-list"> | 353 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 314 | <li data-k="go home" data-cursor> | 354 | <li data-k="go home" data-cursor> | ||
| 315 | <a href="/" tabindex="-1"> | 355 | <a href="/" tabindex="-1"> | ||
| 316 | - | <span class="k">go</span>home</a> | 356 | + | <span class="lbl">home</span> |
| 357 | + | <span class="k">go</span> | |||
| 358 | + | </a> | |||
| 317 | </li> | 359 | </li> | ||
| 318 | <li data-k="section summary"> | 360 | <li data-k="section summary"> | ||
| 319 | <a href="/demo" tabindex="-1"> | 361 | <a href="/demo" tabindex="-1"> | ||
| 320 | - | <span class="k">section</span>summary</a> | 362 | + | <span class="lbl">summary</span> |
| 363 | + | <span class="k">section</span> | |||
| 364 | + | </a> | |||
| 321 | </li> | 365 | </li> | ||
| 322 | - | <li data-k="section tree"> | 366 | + | <li data-k="section files"> |
| 323 | <a href="/demo/tree/main" tabindex="-1"> | 367 | <a href="/demo/tree/main" tabindex="-1"> | ||
| 324 | - | <span class="k">section</span>tree</a> | 368 | + | <span class="lbl">files</span> |
| 369 | + | <span class="k">section</span> | |||
| 370 | + | </a> | |||
| 325 | </li> | 371 | </li> | ||
| 326 | <li data-k="section commits"> | 372 | <li data-k="section commits"> | ||
| 327 | <a href="/demo/commits/main" tabindex="-1"> | 373 | <a href="/demo/commits/main" tabindex="-1"> | ||
| 328 | - | <span class="k">section</span>commits</a> | 374 | + | <span class="lbl">commits</span> |
| 375 | + | <span class="k">section</span> | |||
| 376 | + | </a> | |||
| 329 | </li> | 377 | </li> | ||
| 330 | - | <li data-k="section refs"> | 378 | + | <li data-k="section branches & tags"> |
| 331 | <a href="/demo/refs" tabindex="-1"> | 379 | <a href="/demo/refs" tabindex="-1"> | ||
| 332 | - | <span class="k">section</span>refs</a> | 380 | + | <span class="lbl">branches & tags</span> |
| 381 | + | <span class="k">section</span> | |||
| 382 | + | </a> | |||
| 333 | </li> | 383 | </li> | ||
| 334 | <li data-k="section merge requests"> | 384 | <li data-k="section merge requests"> | ||
| 335 | <a href="/demo/merge-requests" tabindex="-1"> | 385 | <a href="/demo/merge-requests" tabindex="-1"> | ||
| 336 | - | <span class="k">section</span>merge requests</a> | 386 | + | <span class="lbl">merge requests</span> |
| 387 | + | <span class="k">section</span> | |||
| 388 | + | </a> | |||
| 337 | </li> | 389 | </li> | ||
| 338 | <li data-k="branch feature/x"> | 390 | <li data-k="branch feature/x"> | ||
| 339 | <a href="/demo/tree/feature/x" tabindex="-1"> | 391 | <a href="/demo/tree/feature/x" tabindex="-1"> | ||
| 340 | - | <span class="k">branch</span>feature/x</a> | 392 | + | <span class="lbl">feature/x</span> |
| 393 | + | <span class="k">branch</span> | |||
| 394 | + | </a> | |||
| 341 | </li> | 395 | </li> | ||
| 342 | <li data-k="branch main"> | 396 | <li data-k="branch main"> | ||
| 343 | <a href="/demo/tree/main" tabindex="-1"> | 397 | <a href="/demo/tree/main" tabindex="-1"> | ||
| 344 | - | <span class="k">branch</span>main</a> | 398 | + | <span class="lbl">main</span> |
| 399 | + | <span class="k">branch</span> | |||
| 400 | + | </a> | |||
| 345 | </li> | 401 | </li> | ||
| 346 | <li data-k="tag v0.1.0"> | 402 | <li data-k="tag v0.1.0"> | ||
| 347 | <a href="/demo/tree/v0.1.0" tabindex="-1"> | 403 | <a href="/demo/tree/v0.1.0" tabindex="-1"> | ||
| 348 | - | <span class="k">tag</span>v0.1.0</a> | 404 | + | <span class="lbl">v0.1.0</span> |
| 405 | + | <span class="k">tag</span> | |||
| 406 | + | </a> | |||
| 349 | </li> | 407 | </li> | ||
| 350 | <li data-k="tag v0.2.0"> | 408 | <li data-k="tag v0.2.0"> | ||
| 351 | <a href="/demo/tree/v0.2.0" tabindex="-1"> | 409 | <a href="/demo/tree/v0.2.0" tabindex="-1"> | ||
| 352 | - | <span class="k">tag</span>v0.2.0</a> | 410 | + | <span class="lbl">v0.2.0</span> |
| 411 | + | <span class="k">tag</span> | |||
| 412 | + | </a> | |||
| 353 | </li> | 413 | </li> | ||
| 354 | <li data-k="repo quiet"> | 414 | <li data-k="repo quiet"> | ||
| 355 | <a href="/quiet" tabindex="-1"> | 415 | <a href="/quiet" tabindex="-1"> | ||
| 356 | - | <span class="k">repo</span>quiet</a> | 416 | + | <span class="lbl">quiet</span> |
| 417 | + | <span class="k">repo</span> | |||
| 418 | + | </a> | |||
| 357 | </li> | 419 | </li> | ||
| 358 | </ul> | 420 | </ul> | ||
| 421 | + | <p class="panel-foot"> | |||
| 422 | + | <span> | |||
| 423 | + | <kbd>↑</kbd> | |||
| 424 | + | <kbd>↓</kbd> move</span> | |||
| 425 | + | <span> | |||
| 426 | + | <kbd>enter</kbd> open</span> | |||
| 427 | + | <span> | |||
| 428 | + | <kbd>esc</kbd> close</span> | |||
| 429 | + | </p> | |||
| 359 | </dialog> | 430 | </dialog> | ||
| 360 | - | <footer class="bar status" aria-label="Status"> | 431 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 361 | - | <span class="mode">summary</span> | |||
| 362 | - | <span class="st-ctx">2 recent commits</span> | |||
| 363 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 364 | - | <span class="hi">:</span> go to…</button> | |||
| 365 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 366 | - | </span> | |||
| 367 | - | <span class="st-hints">j/k move · enter open · b ref · y permalink · ? help</span> | |||
| 368 | - | <span class="st-src"> | |||
| 369 | - | <span class="st-ver">ermite devel · </span> | |||
| 370 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 371 | - | </span> | |||
| 372 | - | <span class="st-live" data-state="offline" role="status" hx-sse:connect="/-/events?topic=repo%3Ademo&view=summary" hx-swap="none" :text="data.state" :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |||
| 373 | - | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |||
| 374 | - | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |||
| 375 | - | gap -> ermiteLive.reload()">offline</span> | |||
| 376 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/2</span> | |||
| 377 | - | </footer> | |||
| 378 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 379 | if (q('dialog[open]').count) return; | 432 | if (q('dialog[open]').count) return; | ||
| 380 | let rows = q('[data-rows] > :not([hidden])').arr(); | 433 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 381 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 434 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 382 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 435 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 383 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 436 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 384 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 437 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 438 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 439 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 385 | })(1); | 440 | })(1); | ||
| 386 | - | k-k from:body -> ((d) => { | 441 | + | k-k from:body -> (async (d) => { |
| 387 | if (q('dialog[open]').count) return; | 442 | if (q('dialog[open]').count) return; | ||
| 388 | let rows = q('[data-rows] > :not([hidden])').arr(); | 443 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 389 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 444 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 390 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 445 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 391 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 446 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 392 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 447 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 448 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 449 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 393 | })(-1); | 450 | })(-1); | ||
| 394 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 451 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 395 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 452 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 396 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 453 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 397 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 454 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 398 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 455 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 399 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 456 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 400 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 457 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 401 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 458 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 402 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 459 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 403 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 460 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 404 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 461 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 405 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 462 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 406 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 463 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 407 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 464 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 408 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 465 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 409 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 466 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 410 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 467 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 411 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 468 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 412 | k-next from:body -> { | 469 | k-next from:body -> { | ||
| 413 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 470 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 414 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 471 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/tree-nested.html +162 -110
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>src/lib · files · example.test</title> | 7 | <title>src/lib · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -41,33 +45,52 @@ | |||||
| 41 | </li> | 45 | </li> | ||
| 42 | </ol> | 46 | </ol> | ||
| 43 | </nav> | 47 | </nav> | ||
| 44 | - | <nav class="secs" aria-label="Sections"> | 48 | + | <div class="hdr-acts"> |
| 49 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 50 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="ref-name">main</span> | |||
| 53 | + | <span class="sha">SHA7</span> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 56 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 57 | + | </span> | |||
| 58 | + | <span class="search-label">Go to…</span> | |||
| 59 | + | <kbd>ctrl k</kbd> | |||
| 60 | + | </button> | |||
| 61 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 62 | + | </div> | |||
| 63 | + | </div> | |||
| 64 | + | <nav class="tabs" aria-label="Sections"> | |||
| 45 | <ul> | 65 | <ul> | ||
| 46 | <li> | 66 | <li> | ||
| 47 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 67 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 68 | + | </a> | |||
| 48 | </li> | 69 | </li> | ||
| 49 | <li> | 70 | <li> | ||
| 50 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 71 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 72 | + | </a> | |||
| 51 | </li> | 73 | </li> | ||
| 52 | <li> | 74 | <li> | ||
| 53 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 75 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 76 | + | </a> | |||
| 54 | </li> | 77 | </li> | ||
| 55 | <li> | 78 | <li> | ||
| 56 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 79 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 80 | + | </a> | |||
| 57 | </li> | 81 | </li> | ||
| 58 | <li> | 82 | <li> | ||
| 59 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 83 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 84 | + | </a> | |||
| 60 | </li> | 85 | </li> | ||
| 61 | </ul> | 86 | </ul> | ||
| 62 | </nav> | 87 | </nav> | ||
| 63 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 64 | - | </button> | |||
| 65 | </header> | 88 | </header> | ||
| 66 | <div class="work"> | 89 | <div class="work"> | ||
| 67 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 90 | + | <main id="main" tabindex="-1"> |
| 68 | <div class="filter"> | 91 | <div class="filter"> | ||
| 69 | - | <label for="tree-filter">filter></label> | 92 | + | <label for="tree-filter" class="sr">filter</label> |
| 70 | - | <input id="tree-filter" type="search" data-filter placeholder="this directory ( / )" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')"> | 93 | + | <input id="tree-filter" type="search" data-filter placeholder="Filter this directory…" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')"> |
| 71 | </div> | 94 | </div> | ||
| 72 | <table class="list tree" aria-label="Entries"> | 95 | <table class="list tree" aria-label="Entries"> | ||
| 73 | <thead> | 96 | <thead> | ||
| @@ -104,70 +127,71 @@ | |||||
| 104 | </table> | 127 | </table> | ||
| 105 | </main> | 128 | </main> | ||
| 106 | </div> | 129 | </div> | ||
| 107 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 130 | + | <footer class="foot"> |
| 131 | + | <span>ermite devel</span> | |||
| 132 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 133 | + | <span class="foot-keys"> | |||
| 134 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 135 | + | </footer> | |||
| 136 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 137 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 138 | + | </span> | |||
| 139 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 140 | + | </span> | |||
| 141 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 142 | + | </span> | |||
| 143 | + | </div> | |||
| 144 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 108 | <div class="panel-head"> | 145 | <div class="panel-head"> | ||
| 109 | - | <span>keys</span> | 146 | + | <h2>Keyboard shortcuts</h2> |
| 110 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 147 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 148 | + | <kbd>esc</kbd> close</button> | |||
| 111 | </div> | 149 | </div> | ||
| 112 | <dl class="keys"> | 150 | <dl class="keys"> | ||
| 113 | <div> | 151 | <div> | ||
| 114 | <dt> | 152 | <dt> | ||
| 115 | - | <kbd>j</kbd> | 153 | + | <kbd>ctrl k</kbd> |
| 116 | - | <kbd>k</kbd> | 154 | + | <kbd>:</kbd> |
| 117 | </dt> | 155 | </dt> | ||
| 118 | - | <dd>move cursor down / up</dd> | 156 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 119 | </div> | 157 | </div> | ||
| 120 | <div> | 158 | <div> | ||
| 121 | <dt> | 159 | <dt> | ||
| 122 | - | <kbd>enter</kbd> | 160 | + | <kbd>?</kbd> |
| 123 | </dt> | 161 | </dt> | ||
| 124 | - | <dd>open row under cursor</dd> | 162 | + | <dd>this list</dd> |
| 125 | </div> | 163 | </div> | ||
| 126 | <div> | 164 | <div> | ||
| 127 | <dt> | 165 | <dt> | ||
| 128 | - | <kbd>g</kbd> | 166 | + | <kbd>j</kbd> |
| 129 | - | <kbd>g</kbd> / <kbd>G</kbd> | 167 | + | <kbd>k</kbd> |
| 130 | </dt> | 168 | </dt> | ||
| 131 | - | <dd>first / last row</dd> | 169 | + | <dd>move the row cursor down / up</dd> |
| 132 | </div> | 170 | </div> | ||
| 133 | <div> | 171 | <div> | ||
| 134 | <dt> | 172 | <dt> | ||
| 135 | - | <kbd>/</kbd> | 173 | + | <kbd>enter</kbd> |
| 136 | </dt> | 174 | </dt> | ||
| 137 | - | <dd>focus the filter</dd> | 175 | + | <dd>open the row under the cursor</dd> |
| 138 | </div> | 176 | </div> | ||
| 139 | <div> | 177 | <div> | ||
| 140 | <dt> | 178 | <dt> | ||
| 141 | - | <kbd>g h</kbd> | 179 | + | <kbd>g g</kbd> |
| 142 | - | <kbd>g t</kbd> | 180 | + | <kbd>G</kbd> |
| 143 | </dt> | 181 | </dt> | ||
| 144 | - | <dd>go home / tree</dd> | 182 | + | <dd>first / last row</dd> |
| 145 | </div> | 183 | </div> | ||
| 146 | <div> | 184 | <div> | ||
| 147 | <dt> | 185 | <dt> | ||
| 148 | - | <kbd>g c</kbd> | 186 | + | <kbd>/</kbd> |
| 149 | - | <kbd>g r</kbd> | |||
| 150 | </dt> | 187 | </dt> | ||
| 151 | - | <dd>go commits / refs</dd> | 188 | + | <dd>filter the list</dd> |
| 152 | </div> | 189 | </div> | ||
| 153 | <div> | 190 | <div> | ||
| 154 | <dt> | 191 | <dt> | ||
| 155 | <kbd>b</kbd> | 192 | <kbd>b</kbd> | ||
| 156 | </dt> | 193 | </dt> | ||
| 157 | - | <dd>switch ref (branches, tags)</dd> | 194 | + | <dd>switch branch or tag</dd> |
| 158 | - | </div> | |||
| 159 | - | <div> | |||
| 160 | - | <dt> | |||
| 161 | - | <kbd>:</kbd> | |||
| 162 | - | <kbd>f</kbd> | |||
| 163 | - | </dt> | |||
| 164 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 165 | - | </div> | |||
| 166 | - | <div> | |||
| 167 | - | <dt> | |||
| 168 | - | <kbd>y</kbd> | |||
| 169 | - | </dt> | |||
| 170 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 171 | </div> | 195 | </div> | ||
| 172 | <div> | 196 | <div> | ||
| 173 | <dt> | 197 | <dt> | ||
| @@ -175,15 +199,21 @@ | |||||
| 175 | <kbd>t</kbd> | 199 | <kbd>t</kbd> | ||
| 176 | <kbd>c</kbd> | 200 | <kbd>c</kbd> | ||
| 177 | <kbd>r</kbd> | 201 | <kbd>r</kbd> | ||
| 202 | + | <kbd>m</kbd> | |||
| 178 | </dt> | 203 | </dt> | ||
| 179 | - | <dd>repo sections (when shown)</dd> | 204 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 180 | </div> | 205 | </div> | ||
| 181 | <div> | 206 | <div> | ||
| 182 | <dt> | 207 | <dt> | ||
| 183 | - | <kbd>m</kbd> | 208 | + | <kbd>g h</kbd> |
| 184 | - | <kbd>g m</kbd> | |||
| 185 | </dt> | 209 | </dt> | ||
| 186 | - | <dd>merge requests (when shown)</dd> | 210 | + | <dd>home</dd> |
| 211 | + | </div> | |||
| 212 | + | <div> | |||
| 213 | + | <dt> | |||
| 214 | + | <kbd>y</kbd> | |||
| 215 | + | </dt> | |||
| 216 | + | <dd>copy a permalink pinned to the commit</dd> | |||
| 187 | </div> | 217 | </div> | ||
| 188 | <div> | 218 | <div> | ||
| 189 | <dt> | 219 | <dt> | ||
| @@ -203,19 +233,21 @@ | |||||
| 203 | <dt> | 233 | <dt> | ||
| 204 | <kbd>esc</kbd> | 234 | <kbd>esc</kbd> | ||
| 205 | </dt> | 235 | </dt> | ||
| 206 | - | <dd>close panel</dd> | 236 | + | <dd>close a dialog, leave a field</dd> |
| 207 | </div> | 237 | </div> | ||
| 208 | </dl> | 238 | </dl> | ||
| 209 | </dialog> | 239 | </dialog> | ||
| 210 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 240 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 211 | <div class="panel-head"> | 241 | <div class="panel-head"> | ||
| 212 | - | <label for="refs-filter">ref></label> | 242 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 213 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 243 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 214 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 244 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 245 | + | <kbd>esc</kbd> | |||
| 246 | + | </button> | |||
| 215 | </div> | 247 | </div> | ||
| 216 | <div class="panel-cols" data-picklist> | 248 | <div class="panel-cols" data-picklist> | ||
| 217 | <section aria-label="Branches"> | 249 | <section aria-label="Branches"> | ||
| 218 | - | <h2>branches <span class="mute">2</span> | 250 | + | <h2>Branches <span class="count">2</span> |
| 219 | </h2> | 251 | </h2> | ||
| 220 | <ul> | 252 | <ul> | ||
| 221 | <li> | 253 | <li> | ||
| @@ -229,7 +261,7 @@ | |||||
| 229 | </ul> | 261 | </ul> | ||
| 230 | </section> | 262 | </section> | ||
| 231 | <section aria-label="Tags"> | 263 | <section aria-label="Tags"> | ||
| 232 | - | <h2>tags <span class="mute">1</span> | 264 | + | <h2>Tags <span class="count">1</span> |
| 233 | </h2> | 265 | </h2> | ||
| 234 | <ul> | 266 | <ul> | ||
| 235 | <li> | 267 | <li> | ||
| @@ -240,110 +272,130 @@ | |||||
| 240 | </section> | 272 | </section> | ||
| 241 | </div> | 273 | </div> | ||
| 242 | </dialog> | 274 | </dialog> | ||
| 243 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 275 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 244 | <div class="panel-head"> | 276 | <div class="panel-head"> | ||
| 245 | - | <label for="cmd-q" class="hi">:</label> | 277 | + | <label for="cmd-q" class="sr">go to</label> |
| 246 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 278 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 247 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 279 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 248 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 280 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 249 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 281 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 250 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 282 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 251 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 283 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 252 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 284 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 253 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 285 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 286 | + | <kbd>esc</kbd> | |||
| 287 | + | </button> | |||
| 254 | </div> | 288 | </div> | ||
| 255 | - | <ul id="cmd-list" class="cmd-list"> | 289 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 256 | <li data-k="go home" data-cursor> | 290 | <li data-k="go home" data-cursor> | ||
| 257 | <a href="/" tabindex="-1"> | 291 | <a href="/" tabindex="-1"> | ||
| 258 | - | <span class="k">go</span>home</a> | 292 | + | <span class="lbl">home</span> |
| 293 | + | <span class="k">go</span> | |||
| 294 | + | </a> | |||
| 259 | </li> | 295 | </li> | ||
| 260 | <li data-k="section summary"> | 296 | <li data-k="section summary"> | ||
| 261 | <a href="/files" tabindex="-1"> | 297 | <a href="/files" tabindex="-1"> | ||
| 262 | - | <span class="k">section</span>summary</a> | 298 | + | <span class="lbl">summary</span> |
| 299 | + | <span class="k">section</span> | |||
| 300 | + | </a> | |||
| 263 | </li> | 301 | </li> | ||
| 264 | - | <li data-k="section tree"> | 302 | + | <li data-k="section files"> |
| 265 | <a href="/files/tree/main" tabindex="-1"> | 303 | <a href="/files/tree/main" tabindex="-1"> | ||
| 266 | - | <span class="k">section</span>tree</a> | 304 | + | <span class="lbl">files</span> |
| 305 | + | <span class="k">section</span> | |||
| 306 | + | </a> | |||
| 267 | </li> | 307 | </li> | ||
| 268 | <li data-k="section commits"> | 308 | <li data-k="section commits"> | ||
| 269 | <a href="/files/commits/main" tabindex="-1"> | 309 | <a href="/files/commits/main" tabindex="-1"> | ||
| 270 | - | <span class="k">section</span>commits</a> | 310 | + | <span class="lbl">commits</span> |
| 311 | + | <span class="k">section</span> | |||
| 312 | + | </a> | |||
| 271 | </li> | 313 | </li> | ||
| 272 | - | <li data-k="section refs"> | 314 | + | <li data-k="section branches & tags"> |
| 273 | <a href="/files/refs" tabindex="-1"> | 315 | <a href="/files/refs" tabindex="-1"> | ||
| 274 | - | <span class="k">section</span>refs</a> | 316 | + | <span class="lbl">branches & tags</span> |
| 317 | + | <span class="k">section</span> | |||
| 318 | + | </a> | |||
| 275 | </li> | 319 | </li> | ||
| 276 | <li data-k="section merge requests"> | 320 | <li data-k="section merge requests"> | ||
| 277 | <a href="/files/merge-requests" tabindex="-1"> | 321 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 278 | - | <span class="k">section</span>merge requests</a> | 322 | + | <span class="lbl">merge requests</span> |
| 323 | + | <span class="k">section</span> | |||
| 324 | + | </a> | |||
| 279 | </li> | 325 | </li> | ||
| 280 | <li data-k="branch feature/x"> | 326 | <li data-k="branch feature/x"> | ||
| 281 | <a href="/files/tree/feature/x" tabindex="-1"> | 327 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 282 | - | <span class="k">branch</span>feature/x</a> | 328 | + | <span class="lbl">feature/x</span> |
| 329 | + | <span class="k">branch</span> | |||
| 330 | + | </a> | |||
| 283 | </li> | 331 | </li> | ||
| 284 | <li data-k="branch main"> | 332 | <li data-k="branch main"> | ||
| 285 | <a href="/files/tree/main" tabindex="-1"> | 333 | <a href="/files/tree/main" tabindex="-1"> | ||
| 286 | - | <span class="k">branch</span>main</a> | 334 | + | <span class="lbl">main</span> |
| 335 | + | <span class="k">branch</span> | |||
| 336 | + | </a> | |||
| 287 | </li> | 337 | </li> | ||
| 288 | <li data-k="tag v1.0"> | 338 | <li data-k="tag v1.0"> | ||
| 289 | <a href="/files/tree/v1.0" tabindex="-1"> | 339 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 290 | - | <span class="k">tag</span>v1.0</a> | 340 | + | <span class="lbl">v1.0</span> |
| 341 | + | <span class="k">tag</span> | |||
| 342 | + | </a> | |||
| 291 | </li> | 343 | </li> | ||
| 292 | <li data-k="file src/lib/util.go"> | 344 | <li data-k="file src/lib/util.go"> | ||
| 293 | <a href="/files/blob/main/src/lib/util.go" tabindex="-1"> | 345 | <a href="/files/blob/main/src/lib/util.go" tabindex="-1"> | ||
| 294 | - | <span class="k">file</span>src/lib/util.go</a> | 346 | + | <span class="lbl">src/lib/util.go</span> |
| 347 | + | <span class="k">file</span> | |||
| 348 | + | </a> | |||
| 295 | </li> | 349 | </li> | ||
| 296 | </ul> | 350 | </ul> | ||
| 351 | + | <p class="panel-foot"> | |||
| 352 | + | <span> | |||
| 353 | + | <kbd>↑</kbd> | |||
| 354 | + | <kbd>↓</kbd> move</span> | |||
| 355 | + | <span> | |||
| 356 | + | <kbd>enter</kbd> open</span> | |||
| 357 | + | <span> | |||
| 358 | + | <kbd>esc</kbd> close</span> | |||
| 359 | + | </p> | |||
| 297 | </dialog> | 360 | </dialog> | ||
| 298 | - | <footer class="bar status" aria-label="Status"> | 361 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 299 | - | <span class="mode">tree</span> | |||
| 300 | - | <span class="st-ctx">tree · 1 entry</span> | |||
| 301 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 302 | - | <span class="hi">:</span> go to…</button> | |||
| 303 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 304 | - | </span> | |||
| 305 | - | <span class="st-hints">j/k move · enter open · / filter · b ref · ? help</span> | |||
| 306 | - | <span class="st-src"> | |||
| 307 | - | <span class="st-ver">ermite devel · </span> | |||
| 308 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 309 | - | </span> | |||
| 310 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 311 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/1</span> | |||
| 312 | - | </footer> | |||
| 313 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 314 | if (q('dialog[open]').count) return; | 362 | if (q('dialog[open]').count) return; | ||
| 315 | let rows = q('[data-rows] > :not([hidden])').arr(); | 363 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 316 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 364 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 317 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 365 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 318 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 366 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 319 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 367 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 368 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 369 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 320 | })(1); | 370 | })(1); | ||
| 321 | - | k-k from:body -> ((d) => { | 371 | + | k-k from:body -> (async (d) => { |
| 322 | if (q('dialog[open]').count) return; | 372 | if (q('dialog[open]').count) return; | ||
| 323 | let rows = q('[data-rows] > :not([hidden])').arr(); | 373 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 324 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 374 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 325 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 375 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 326 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 376 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 327 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 377 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 378 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 379 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 328 | })(-1); | 380 | })(-1); | ||
| 329 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 381 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 330 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 382 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 331 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 383 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 332 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 384 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 333 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 385 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 334 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 386 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 335 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 387 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 336 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 388 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 337 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 389 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 338 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 390 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 339 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 391 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 340 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 392 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 341 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 393 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 342 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 394 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 343 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 395 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 344 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 396 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 345 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 397 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 346 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 398 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 347 | k-next from:body -> { | 399 | k-next from:body -> { | ||
| 348 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 400 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 349 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 401 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/tree-root.html +203 -125
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>tree · files · example.test</title> | 7 | <title>tree · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -35,33 +39,52 @@ | |||||
| 35 | </li> | 39 | </li> | ||
| 36 | </ol> | 40 | </ol> | ||
| 37 | </nav> | 41 | </nav> | ||
| 38 | - | <nav class="secs" aria-label="Sections"> | 42 | + | <div class="hdr-acts"> |
| 43 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 44 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 45 | + | </span> | |||
| 46 | + | <span class="ref-name">main</span> | |||
| 47 | + | <span class="sha">SHA7</span> | |||
| 48 | + | </button> | |||
| 49 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 50 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 51 | + | </span> | |||
| 52 | + | <span class="search-label">Go to…</span> | |||
| 53 | + | <kbd>ctrl k</kbd> | |||
| 54 | + | </button> | |||
| 55 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 56 | + | </div> | |||
| 57 | + | </div> | |||
| 58 | + | <nav class="tabs" aria-label="Sections"> | |||
| 39 | <ul> | 59 | <ul> | ||
| 40 | <li> | 60 | <li> | ||
| 41 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 61 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 62 | + | </a> | |||
| 42 | </li> | 63 | </li> | ||
| 43 | <li> | 64 | <li> | ||
| 44 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 65 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 66 | + | </a> | |||
| 45 | </li> | 67 | </li> | ||
| 46 | <li> | 68 | <li> | ||
| 47 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 69 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 70 | + | </a> | |||
| 48 | </li> | 71 | </li> | ||
| 49 | <li> | 72 | <li> | ||
| 50 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 73 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 74 | + | </a> | |||
| 51 | </li> | 75 | </li> | ||
| 52 | <li> | 76 | <li> | ||
| 53 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 77 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 78 | + | </a> | |||
| 54 | </li> | 79 | </li> | ||
| 55 | </ul> | 80 | </ul> | ||
| 56 | </nav> | 81 | </nav> | ||
| 57 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 58 | - | </button> | |||
| 59 | </header> | 82 | </header> | ||
| 60 | <div class="work"> | 83 | <div class="work"> | ||
| 61 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 84 | + | <main id="main" tabindex="-1"> |
| 62 | <div class="filter"> | 85 | <div class="filter"> | ||
| 63 | - | <label for="tree-filter">filter></label> | 86 | + | <label for="tree-filter" class="sr">filter</label> |
| 64 | - | <input id="tree-filter" type="search" data-filter placeholder="this directory ( / )" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')"> | 87 | + | <input id="tree-filter" type="search" data-filter placeholder="Filter this directory…" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')"> |
| 65 | </div> | 88 | </div> | ||
| 66 | <table class="list tree" aria-label="Entries"> | 89 | <table class="list tree" aria-label="Entries"> | ||
| 67 | <thead> | 90 | <thead> | ||
| @@ -320,79 +343,82 @@ | |||||
| 320 | </tr> | 343 | </tr> | ||
| 321 | </tbody> | 344 | </tbody> | ||
| 322 | </table> | 345 | </table> | ||
| 323 | - | <h2 class="rule-h"> | 346 | + | <section class="card readme" aria-labelledby="readme-h"> |
| 324 | - | <span>README.md</span> | 347 | + | <h2 class="card-h" id="readme-h"> |
| 325 | - | </h2> | 348 | + | <span class="ico i-file" aria-hidden="true"> |
| 349 | + | </span> README.md</h2> | |||
| 326 | <article class="prose" aria-label="README.md"> | 350 | <article class="prose" aria-label="README.md"> | ||
| 327 | <h1 id="md-files">Files</h1> | 351 | <h1 id="md-files">Files</h1> | ||
| 328 | <p>See <a href="/files/blob/main/docs/guide.md">the guide</a>.</p> | 352 | <p>See <a href="/files/blob/main/docs/guide.md">the guide</a>.</p> | ||
| 329 | </article> | 353 | </article> | ||
| 354 | + | </section> | |||
| 330 | </main> | 355 | </main> | ||
| 331 | </div> | 356 | </div> | ||
| 332 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 357 | + | <footer class="foot"> |
| 358 | + | <span>ermite devel</span> | |||
| 359 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 360 | + | <span class="foot-keys"> | |||
| 361 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 362 | + | </footer> | |||
| 363 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 364 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 365 | + | </span> | |||
| 366 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 367 | + | </span> | |||
| 368 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 369 | + | </span> | |||
| 370 | + | </div> | |||
| 371 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 333 | <div class="panel-head"> | 372 | <div class="panel-head"> | ||
| 334 | - | <span>keys</span> | 373 | + | <h2>Keyboard shortcuts</h2> |
| 335 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 374 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 375 | + | <kbd>esc</kbd> close</button> | |||
| 336 | </div> | 376 | </div> | ||
| 337 | <dl class="keys"> | 377 | <dl class="keys"> | ||
| 338 | <div> | 378 | <div> | ||
| 339 | <dt> | 379 | <dt> | ||
| 340 | - | <kbd>j</kbd> | 380 | + | <kbd>ctrl k</kbd> |
| 341 | - | <kbd>k</kbd> | 381 | + | <kbd>:</kbd> |
| 342 | </dt> | 382 | </dt> | ||
| 343 | - | <dd>move cursor down / up</dd> | 383 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 344 | </div> | 384 | </div> | ||
| 345 | <div> | 385 | <div> | ||
| 346 | <dt> | 386 | <dt> | ||
| 347 | - | <kbd>enter</kbd> | 387 | + | <kbd>?</kbd> |
| 348 | </dt> | 388 | </dt> | ||
| 349 | - | <dd>open row under cursor</dd> | 389 | + | <dd>this list</dd> |
| 350 | </div> | 390 | </div> | ||
| 351 | <div> | 391 | <div> | ||
| 352 | <dt> | 392 | <dt> | ||
| 353 | - | <kbd>g</kbd> | 393 | + | <kbd>j</kbd> |
| 354 | - | <kbd>g</kbd> / <kbd>G</kbd> | 394 | + | <kbd>k</kbd> |
| 355 | </dt> | 395 | </dt> | ||
| 356 | - | <dd>first / last row</dd> | 396 | + | <dd>move the row cursor down / up</dd> |
| 357 | </div> | 397 | </div> | ||
| 358 | <div> | 398 | <div> | ||
| 359 | <dt> | 399 | <dt> | ||
| 360 | - | <kbd>/</kbd> | 400 | + | <kbd>enter</kbd> |
| 361 | </dt> | 401 | </dt> | ||
| 362 | - | <dd>focus the filter</dd> | 402 | + | <dd>open the row under the cursor</dd> |
| 363 | </div> | 403 | </div> | ||
| 364 | <div> | 404 | <div> | ||
| 365 | <dt> | 405 | <dt> | ||
| 366 | - | <kbd>g h</kbd> | 406 | + | <kbd>g g</kbd> |
| 367 | - | <kbd>g t</kbd> | 407 | + | <kbd>G</kbd> |
| 368 | </dt> | 408 | </dt> | ||
| 369 | - | <dd>go home / tree</dd> | 409 | + | <dd>first / last row</dd> |
| 370 | </div> | 410 | </div> | ||
| 371 | <div> | 411 | <div> | ||
| 372 | <dt> | 412 | <dt> | ||
| 373 | - | <kbd>g c</kbd> | 413 | + | <kbd>/</kbd> |
| 374 | - | <kbd>g r</kbd> | |||
| 375 | </dt> | 414 | </dt> | ||
| 376 | - | <dd>go commits / refs</dd> | 415 | + | <dd>filter the list</dd> |
| 377 | </div> | 416 | </div> | ||
| 378 | <div> | 417 | <div> | ||
| 379 | <dt> | 418 | <dt> | ||
| 380 | <kbd>b</kbd> | 419 | <kbd>b</kbd> | ||
| 381 | </dt> | 420 | </dt> | ||
| 382 | - | <dd>switch ref (branches, tags)</dd> | 421 | + | <dd>switch branch or tag</dd> |
| 383 | - | </div> | |||
| 384 | - | <div> | |||
| 385 | - | <dt> | |||
| 386 | - | <kbd>:</kbd> | |||
| 387 | - | <kbd>f</kbd> | |||
| 388 | - | </dt> | |||
| 389 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 390 | - | </div> | |||
| 391 | - | <div> | |||
| 392 | - | <dt> | |||
| 393 | - | <kbd>y</kbd> | |||
| 394 | - | </dt> | |||
| 395 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 396 | </div> | 422 | </div> | ||
| 397 | <div> | 423 | <div> | ||
| 398 | <dt> | 424 | <dt> | ||
| @@ -400,15 +426,21 @@ | |||||
| 400 | <kbd>t</kbd> | 426 | <kbd>t</kbd> | ||
| 401 | <kbd>c</kbd> | 427 | <kbd>c</kbd> | ||
| 402 | <kbd>r</kbd> | 428 | <kbd>r</kbd> | ||
| 429 | + | <kbd>m</kbd> | |||
| 403 | </dt> | 430 | </dt> | ||
| 404 | - | <dd>repo sections (when shown)</dd> | 431 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 405 | </div> | 432 | </div> | ||
| 406 | <div> | 433 | <div> | ||
| 407 | <dt> | 434 | <dt> | ||
| 408 | - | <kbd>m</kbd> | 435 | + | <kbd>g h</kbd> |
| 409 | - | <kbd>g m</kbd> | |||
| 410 | </dt> | 436 | </dt> | ||
| 411 | - | <dd>merge requests (when shown)</dd> | 437 | + | <dd>home</dd> |
| 438 | + | </div> | |||
| 439 | + | <div> | |||
| 440 | + | <dt> | |||
| 441 | + | <kbd>y</kbd> | |||
| 442 | + | </dt> | |||
| 443 | + | <dd>copy a permalink pinned to the commit</dd> | |||
| 412 | </div> | 444 | </div> | ||
| 413 | <div> | 445 | <div> | ||
| 414 | <dt> | 446 | <dt> | ||
| @@ -428,19 +460,21 @@ | |||||
| 428 | <dt> | 460 | <dt> | ||
| 429 | <kbd>esc</kbd> | 461 | <kbd>esc</kbd> | ||
| 430 | </dt> | 462 | </dt> | ||
| 431 | - | <dd>close panel</dd> | 463 | + | <dd>close a dialog, leave a field</dd> |
| 432 | </div> | 464 | </div> | ||
| 433 | </dl> | 465 | </dl> | ||
| 434 | </dialog> | 466 | </dialog> | ||
| 435 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 467 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 436 | <div class="panel-head"> | 468 | <div class="panel-head"> | ||
| 437 | - | <label for="refs-filter">ref></label> | 469 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 438 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 470 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 439 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 471 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 472 | + | <kbd>esc</kbd> | |||
| 473 | + | </button> | |||
| 440 | </div> | 474 | </div> | ||
| 441 | <div class="panel-cols" data-picklist> | 475 | <div class="panel-cols" data-picklist> | ||
| 442 | <section aria-label="Branches"> | 476 | <section aria-label="Branches"> | ||
| 443 | - | <h2>branches <span class="mute">2</span> | 477 | + | <h2>Branches <span class="count">2</span> |
| 444 | </h2> | 478 | </h2> | ||
| 445 | <ul> | 479 | <ul> | ||
| 446 | <li> | 480 | <li> | ||
| @@ -454,7 +488,7 @@ | |||||
| 454 | </ul> | 488 | </ul> | ||
| 455 | </section> | 489 | </section> | ||
| 456 | <section aria-label="Tags"> | 490 | <section aria-label="Tags"> | ||
| 457 | - | <h2>tags <span class="mute">1</span> | 491 | + | <h2>Tags <span class="count">1</span> |
| 458 | </h2> | 492 | </h2> | ||
| 459 | <ul> | 493 | <ul> | ||
| 460 | <li> | 494 | <li> | ||
| @@ -465,158 +499,202 @@ | |||||
| 465 | </section> | 499 | </section> | ||
| 466 | </div> | 500 | </div> | ||
| 467 | </dialog> | 501 | </dialog> | ||
| 468 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 502 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 469 | <div class="panel-head"> | 503 | <div class="panel-head"> | ||
| 470 | - | <label for="cmd-q" class="hi">:</label> | 504 | + | <label for="cmd-q" class="sr">go to</label> |
| 471 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 505 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 472 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 506 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 473 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 507 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 474 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 508 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 475 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 509 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 476 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 510 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 477 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 511 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 478 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 512 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 513 | + | <kbd>esc</kbd> | |||
| 514 | + | </button> | |||
| 479 | </div> | 515 | </div> | ||
| 480 | - | <ul id="cmd-list" class="cmd-list"> | 516 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 481 | <li data-k="go home" data-cursor> | 517 | <li data-k="go home" data-cursor> | ||
| 482 | <a href="/" tabindex="-1"> | 518 | <a href="/" tabindex="-1"> | ||
| 483 | - | <span class="k">go</span>home</a> | 519 | + | <span class="lbl">home</span> |
| 520 | + | <span class="k">go</span> | |||
| 521 | + | </a> | |||
| 484 | </li> | 522 | </li> | ||
| 485 | <li data-k="section summary"> | 523 | <li data-k="section summary"> | ||
| 486 | <a href="/files" tabindex="-1"> | 524 | <a href="/files" tabindex="-1"> | ||
| 487 | - | <span class="k">section</span>summary</a> | 525 | + | <span class="lbl">summary</span> |
| 526 | + | <span class="k">section</span> | |||
| 527 | + | </a> | |||
| 488 | </li> | 528 | </li> | ||
| 489 | - | <li data-k="section tree"> | 529 | + | <li data-k="section files"> |
| 490 | <a href="/files/tree/main" tabindex="-1"> | 530 | <a href="/files/tree/main" tabindex="-1"> | ||
| 491 | - | <span class="k">section</span>tree</a> | 531 | + | <span class="lbl">files</span> |
| 532 | + | <span class="k">section</span> | |||
| 533 | + | </a> | |||
| 492 | </li> | 534 | </li> | ||
| 493 | <li data-k="section commits"> | 535 | <li data-k="section commits"> | ||
| 494 | <a href="/files/commits/main" tabindex="-1"> | 536 | <a href="/files/commits/main" tabindex="-1"> | ||
| 495 | - | <span class="k">section</span>commits</a> | 537 | + | <span class="lbl">commits</span> |
| 538 | + | <span class="k">section</span> | |||
| 539 | + | </a> | |||
| 496 | </li> | 540 | </li> | ||
| 497 | - | <li data-k="section refs"> | 541 | + | <li data-k="section branches & tags"> |
| 498 | <a href="/files/refs" tabindex="-1"> | 542 | <a href="/files/refs" tabindex="-1"> | ||
| 499 | - | <span class="k">section</span>refs</a> | 543 | + | <span class="lbl">branches & tags</span> |
| 544 | + | <span class="k">section</span> | |||
| 545 | + | </a> | |||
| 500 | </li> | 546 | </li> | ||
| 501 | <li data-k="section merge requests"> | 547 | <li data-k="section merge requests"> | ||
| 502 | <a href="/files/merge-requests" tabindex="-1"> | 548 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 503 | - | <span class="k">section</span>merge requests</a> | 549 | + | <span class="lbl">merge requests</span> |
| 550 | + | <span class="k">section</span> | |||
| 551 | + | </a> | |||
| 504 | </li> | 552 | </li> | ||
| 505 | <li data-k="branch feature/x"> | 553 | <li data-k="branch feature/x"> | ||
| 506 | <a href="/files/tree/feature/x" tabindex="-1"> | 554 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 507 | - | <span class="k">branch</span>feature/x</a> | 555 | + | <span class="lbl">feature/x</span> |
| 556 | + | <span class="k">branch</span> | |||
| 557 | + | </a> | |||
| 508 | </li> | 558 | </li> | ||
| 509 | <li data-k="branch main"> | 559 | <li data-k="branch main"> | ||
| 510 | <a href="/files/tree/main" tabindex="-1"> | 560 | <a href="/files/tree/main" tabindex="-1"> | ||
| 511 | - | <span class="k">branch</span>main</a> | 561 | + | <span class="lbl">main</span> |
| 562 | + | <span class="k">branch</span> | |||
| 563 | + | </a> | |||
| 512 | </li> | 564 | </li> | ||
| 513 | <li data-k="tag v1.0"> | 565 | <li data-k="tag v1.0"> | ||
| 514 | <a href="/files/tree/v1.0" tabindex="-1"> | 566 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 515 | - | <span class="k">tag</span>v1.0</a> | 567 | + | <span class="lbl">v1.0</span> |
| 568 | + | <span class="k">tag</span> | |||
| 569 | + | </a> | |||
| 516 | </li> | 570 | </li> | ||
| 517 | <li data-k="file readme.md"> | 571 | <li data-k="file readme.md"> | ||
| 518 | <a href="/files/blob/main/README.md" tabindex="-1"> | 572 | <a href="/files/blob/main/README.md" tabindex="-1"> | ||
| 519 | - | <span class="k">file</span>README.md</a> | 573 | + | <span class="lbl">README.md</span> |
| 574 | + | <span class="k">file</span> | |||
| 575 | + | </a> | |||
| 520 | </li> | 576 | </li> | ||
| 521 | <li data-k="file big.txt"> | 577 | <li data-k="file big.txt"> | ||
| 522 | <a href="/files/blob/main/big.txt" tabindex="-1"> | 578 | <a href="/files/blob/main/big.txt" tabindex="-1"> | ||
| 523 | - | <span class="k">file</span>big.txt</a> | 579 | + | <span class="lbl">big.txt</span> |
| 580 | + | <span class="k">file</span> | |||
| 581 | + | </a> | |||
| 524 | </li> | 582 | </li> | ||
| 525 | <li data-k="file data.bin"> | 583 | <li data-k="file data.bin"> | ||
| 526 | <a href="/files/blob/main/data.bin" tabindex="-1"> | 584 | <a href="/files/blob/main/data.bin" tabindex="-1"> | ||
| 527 | - | <span class="k">file</span>data.bin</a> | 585 | + | <span class="lbl">data.bin</span> |
| 586 | + | <span class="k">file</span> | |||
| 587 | + | </a> | |||
| 528 | </li> | 588 | </li> | ||
| 529 | <li data-k="file empty.txt"> | 589 | <li data-k="file empty.txt"> | ||
| 530 | <a href="/files/blob/main/empty.txt" tabindex="-1"> | 590 | <a href="/files/blob/main/empty.txt" tabindex="-1"> | ||
| 531 | - | <span class="k">file</span>empty.txt</a> | 591 | + | <span class="lbl">empty.txt</span> |
| 592 | + | <span class="k">file</span> | |||
| 593 | + | </a> | |||
| 532 | </li> | 594 | </li> | ||
| 533 | <li data-k="file evil.go"> | 595 | <li data-k="file evil.go"> | ||
| 534 | <a href="/files/blob/main/evil.go" tabindex="-1"> | 596 | <a href="/files/blob/main/evil.go" tabindex="-1"> | ||
| 535 | - | <span class="k">file</span>evil.go</a> | 597 | + | <span class="lbl">evil.go</span> |
| 598 | + | <span class="k">file</span> | |||
| 599 | + | </a> | |||
| 536 | </li> | 600 | </li> | ||
| 537 | <li data-k="file fake.jpg"> | 601 | <li data-k="file fake.jpg"> | ||
| 538 | <a href="/files/blob/main/fake.jpg" tabindex="-1"> | 602 | <a href="/files/blob/main/fake.jpg" tabindex="-1"> | ||
| 539 | - | <span class="k">file</span>fake.jpg</a> | 603 | + | <span class="lbl">fake.jpg</span> |
| 604 | + | <span class="k">file</span> | |||
| 605 | + | </a> | |||
| 540 | </li> | 606 | </li> | ||
| 541 | <li data-k="file fake.png"> | 607 | <li data-k="file fake.png"> | ||
| 542 | <a href="/files/blob/main/fake.png" tabindex="-1"> | 608 | <a href="/files/blob/main/fake.png" tabindex="-1"> | ||
| 543 | - | <span class="k">file</span>fake.png</a> | 609 | + | <span class="lbl">fake.png</span> |
| 610 | + | <span class="k">file</span> | |||
| 611 | + | </a> | |||
| 544 | </li> | 612 | </li> | ||
| 545 | <li data-k="file lines.txt"> | 613 | <li data-k="file lines.txt"> | ||
| 546 | <a href="/files/blob/main/lines.txt" tabindex="-1"> | 614 | <a href="/files/blob/main/lines.txt" tabindex="-1"> | ||
| 547 | - | <span class="k">file</span>lines.txt</a> | 615 | + | <span class="lbl">lines.txt</span> |
| 616 | + | <span class="k">file</span> | |||
| 617 | + | </a> | |||
| 548 | </li> | 618 | </li> | ||
| 549 | <li data-k="file logo.svg"> | 619 | <li data-k="file logo.svg"> | ||
| 550 | <a href="/files/blob/main/logo.svg" tabindex="-1"> | 620 | <a href="/files/blob/main/logo.svg" tabindex="-1"> | ||
| 551 | - | <span class="k">file</span>logo.svg</a> | 621 | + | <span class="lbl">logo.svg</span> |
| 622 | + | <span class="k">file</span> | |||
| 623 | + | </a> | |||
| 552 | </li> | 624 | </li> | ||
| 553 | <li data-k="file main.go"> | 625 | <li data-k="file main.go"> | ||
| 554 | <a href="/files/blob/main/main.go" tabindex="-1"> | 626 | <a href="/files/blob/main/main.go" tabindex="-1"> | ||
| 555 | - | <span class="k">file</span>main.go</a> | 627 | + | <span class="lbl">main.go</span> |
| 628 | + | <span class="k">file</span> | |||
| 629 | + | </a> | |||
| 556 | </li> | 630 | </li> | ||
| 557 | <li data-k="file notes.txt"> | 631 | <li data-k="file notes.txt"> | ||
| 558 | <a href="/files/blob/main/notes.txt" tabindex="-1"> | 632 | <a href="/files/blob/main/notes.txt" tabindex="-1"> | ||
| 559 | - | <span class="k">file</span>notes.txt</a> | 633 | + | <span class="lbl">notes.txt</span> |
| 634 | + | <span class="k">file</span> | |||
| 635 | + | </a> | |||
| 560 | </li> | 636 | </li> | ||
| 561 | <li data-k="file page.html"> | 637 | <li data-k="file page.html"> | ||
| 562 | <a href="/files/blob/main/page.html" tabindex="-1"> | 638 | <a href="/files/blob/main/page.html" tabindex="-1"> | ||
| 563 | - | <span class="k">file</span>page.html</a> | 639 | + | <span class="lbl">page.html</span> |
| 640 | + | <span class="k">file</span> | |||
| 641 | + | </a> | |||
| 564 | </li> | 642 | </li> | ||
| 565 | <li data-k="file pic.png"> | 643 | <li data-k="file pic.png"> | ||
| 566 | <a href="/files/blob/main/pic.png" tabindex="-1"> | 644 | <a href="/files/blob/main/pic.png" tabindex="-1"> | ||
| 567 | - | <span class="k">file</span>pic.png</a> | 645 | + | <span class="lbl">pic.png</span> |
| 646 | + | <span class="k">file</span> | |||
| 647 | + | </a> | |||
| 568 | </li> | 648 | </li> | ||
| 569 | </ul> | 649 | </ul> | ||
| 650 | + | <p class="panel-foot"> | |||
| 651 | + | <span> | |||
| 652 | + | <kbd>↑</kbd> | |||
| 653 | + | <kbd>↓</kbd> move</span> | |||
| 654 | + | <span> | |||
| 655 | + | <kbd>enter</kbd> open</span> | |||
| 656 | + | <span> | |||
| 657 | + | <kbd>esc</kbd> close</span> | |||
| 658 | + | </p> | |||
| 570 | </dialog> | 659 | </dialog> | ||
| 571 | - | <footer class="bar status" aria-label="Status"> | 660 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 572 | - | <span class="mode">tree</span> | |||
| 573 | - | <span class="st-ctx">tree · 15 entries</span> | |||
| 574 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 575 | - | <span class="hi">:</span> go to…</button> | |||
| 576 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 577 | - | </span> | |||
| 578 | - | <span class="st-hints">j/k move · enter open · / filter · b ref · ? help</span> | |||
| 579 | - | <span class="st-src"> | |||
| 580 | - | <span class="st-ver">ermite devel · </span> | |||
| 581 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 582 | - | </span> | |||
| 583 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 584 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/15</span> | |||
| 585 | - | </footer> | |||
| 586 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 587 | if (q('dialog[open]').count) return; | 661 | if (q('dialog[open]').count) return; | ||
| 588 | let rows = q('[data-rows] > :not([hidden])').arr(); | 662 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 589 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 663 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 590 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 664 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 591 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 665 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 592 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 666 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 667 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 668 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 593 | })(1); | 669 | })(1); | ||
| 594 | - | k-k from:body -> ((d) => { | 670 | + | k-k from:body -> (async (d) => { |
| 595 | if (q('dialog[open]').count) return; | 671 | if (q('dialog[open]').count) return; | ||
| 596 | let rows = q('[data-rows] > :not([hidden])').arr(); | 672 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 597 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 673 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 598 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 674 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 599 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 675 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 600 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 676 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 677 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 678 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 601 | })(-1); | 679 | })(-1); | ||
| 602 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 680 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 603 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 681 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 604 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 682 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 605 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 683 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 606 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 684 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 607 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 685 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 608 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 686 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 609 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 687 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 610 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 688 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 611 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 689 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 612 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 690 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 613 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 691 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 614 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 692 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 615 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 693 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 616 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 694 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 617 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 695 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 618 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 696 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 619 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 697 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 620 | k-next from:body -> { | 698 | k-next from:body -> { | ||
| 621 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 699 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 622 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 700 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/tree-slash-ref.html +162 -110
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>x · files · example.test</title> | 7 | <title>x · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="feature/x" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,33 +42,52 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">feature/x</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 68 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 72 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">feature/x<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work"> | 86 | <div class="work"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <div class="filter"> | 88 | <div class="filter"> | ||
| 66 | - | <label for="tree-filter">filter></label> | 89 | + | <label for="tree-filter" class="sr">filter</label> |
| 67 | - | <input id="tree-filter" type="search" data-filter placeholder="this directory ( / )" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')"> | 90 | + | <input id="tree-filter" type="search" data-filter placeholder="Filter this directory…" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')"> |
| 68 | </div> | 91 | </div> | ||
| 69 | <table class="list tree" aria-label="Entries"> | 92 | <table class="list tree" aria-label="Entries"> | ||
| 70 | <thead> | 93 | <thead> | ||
| @@ -101,70 +124,71 @@ | |||||
| 101 | </table> | 124 | </table> | ||
| 102 | </main> | 125 | </main> | ||
| 103 | </div> | 126 | </div> | ||
| 104 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 127 | + | <footer class="foot"> |
| 128 | + | <span>ermite devel</span> | |||
| 129 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 130 | + | <span class="foot-keys"> | |||
| 131 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 132 | + | </footer> | |||
| 133 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 134 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 135 | + | </span> | |||
| 136 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 137 | + | </span> | |||
| 138 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 139 | + | </span> | |||
| 140 | + | </div> | |||
| 141 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 105 | <div class="panel-head"> | 142 | <div class="panel-head"> | ||
| 106 | - | <span>keys</span> | 143 | + | <h2>Keyboard shortcuts</h2> |
| 107 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 144 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 145 | + | <kbd>esc</kbd> close</button> | |||
| 108 | </div> | 146 | </div> | ||
| 109 | <dl class="keys"> | 147 | <dl class="keys"> | ||
| 110 | <div> | 148 | <div> | ||
| 111 | <dt> | 149 | <dt> | ||
| 112 | - | <kbd>j</kbd> | 150 | + | <kbd>ctrl k</kbd> |
| 113 | - | <kbd>k</kbd> | 151 | + | <kbd>:</kbd> |
| 114 | </dt> | 152 | </dt> | ||
| 115 | - | <dd>move cursor down / up</dd> | 153 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 116 | </div> | 154 | </div> | ||
| 117 | <div> | 155 | <div> | ||
| 118 | <dt> | 156 | <dt> | ||
| 119 | - | <kbd>enter</kbd> | 157 | + | <kbd>?</kbd> |
| 120 | </dt> | 158 | </dt> | ||
| 121 | - | <dd>open row under cursor</dd> | 159 | + | <dd>this list</dd> |
| 122 | </div> | 160 | </div> | ||
| 123 | <div> | 161 | <div> | ||
| 124 | <dt> | 162 | <dt> | ||
| 125 | - | <kbd>g</kbd> | 163 | + | <kbd>j</kbd> |
| 126 | - | <kbd>g</kbd> / <kbd>G</kbd> | 164 | + | <kbd>k</kbd> |
| 127 | </dt> | 165 | </dt> | ||
| 128 | - | <dd>first / last row</dd> | 166 | + | <dd>move the row cursor down / up</dd> |
| 129 | </div> | 167 | </div> | ||
| 130 | <div> | 168 | <div> | ||
| 131 | <dt> | 169 | <dt> | ||
| 132 | - | <kbd>/</kbd> | 170 | + | <kbd>enter</kbd> |
| 133 | </dt> | 171 | </dt> | ||
| 134 | - | <dd>focus the filter</dd> | 172 | + | <dd>open the row under the cursor</dd> |
| 135 | </div> | 173 | </div> | ||
| 136 | <div> | 174 | <div> | ||
| 137 | <dt> | 175 | <dt> | ||
| 138 | - | <kbd>g h</kbd> | 176 | + | <kbd>g g</kbd> |
| 139 | - | <kbd>g t</kbd> | 177 | + | <kbd>G</kbd> |
| 140 | </dt> | 178 | </dt> | ||
| 141 | - | <dd>go home / tree</dd> | 179 | + | <dd>first / last row</dd> |
| 142 | </div> | 180 | </div> | ||
| 143 | <div> | 181 | <div> | ||
| 144 | <dt> | 182 | <dt> | ||
| 145 | - | <kbd>g c</kbd> | 183 | + | <kbd>/</kbd> |
| 146 | - | <kbd>g r</kbd> | |||
| 147 | </dt> | 184 | </dt> | ||
| 148 | - | <dd>go commits / refs</dd> | 185 | + | <dd>filter the list</dd> |
| 149 | </div> | 186 | </div> | ||
| 150 | <div> | 187 | <div> | ||
| 151 | <dt> | 188 | <dt> | ||
| 152 | <kbd>b</kbd> | 189 | <kbd>b</kbd> | ||
| 153 | </dt> | 190 | </dt> | ||
| 154 | - | <dd>switch ref (branches, tags)</dd> | 191 | + | <dd>switch branch or tag</dd> |
| 155 | - | </div> | |||
| 156 | - | <div> | |||
| 157 | - | <dt> | |||
| 158 | - | <kbd>:</kbd> | |||
| 159 | - | <kbd>f</kbd> | |||
| 160 | - | </dt> | |||
| 161 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 162 | - | </div> | |||
| 163 | - | <div> | |||
| 164 | - | <dt> | |||
| 165 | - | <kbd>y</kbd> | |||
| 166 | - | </dt> | |||
| 167 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 168 | </div> | 192 | </div> | ||
| 169 | <div> | 193 | <div> | ||
| 170 | <dt> | 194 | <dt> | ||
| @@ -172,15 +196,21 @@ | |||||
| 172 | <kbd>t</kbd> | 196 | <kbd>t</kbd> | ||
| 173 | <kbd>c</kbd> | 197 | <kbd>c</kbd> | ||
| 174 | <kbd>r</kbd> | 198 | <kbd>r</kbd> | ||
| 199 | + | <kbd>m</kbd> | |||
| 175 | </dt> | 200 | </dt> | ||
| 176 | - | <dd>repo sections (when shown)</dd> | 201 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 177 | </div> | 202 | </div> | ||
| 178 | <div> | 203 | <div> | ||
| 179 | <dt> | 204 | <dt> | ||
| 180 | - | <kbd>m</kbd> | 205 | + | <kbd>g h</kbd> |
| 181 | - | <kbd>g m</kbd> | |||
| 182 | </dt> | 206 | </dt> | ||
| 183 | - | <dd>merge requests (when shown)</dd> | 207 | + | <dd>home</dd> |
| 208 | + | </div> | |||
| 209 | + | <div> | |||
| 210 | + | <dt> | |||
| 211 | + | <kbd>y</kbd> | |||
| 212 | + | </dt> | |||
| 213 | + | <dd>copy a permalink pinned to the commit</dd> | |||
| 184 | </div> | 214 | </div> | ||
| 185 | <div> | 215 | <div> | ||
| 186 | <dt> | 216 | <dt> | ||
| @@ -200,19 +230,21 @@ | |||||
| 200 | <dt> | 230 | <dt> | ||
| 201 | <kbd>esc</kbd> | 231 | <kbd>esc</kbd> | ||
| 202 | </dt> | 232 | </dt> | ||
| 203 | - | <dd>close panel</dd> | 233 | + | <dd>close a dialog, leave a field</dd> |
| 204 | </div> | 234 | </div> | ||
| 205 | </dl> | 235 | </dl> | ||
| 206 | </dialog> | 236 | </dialog> | ||
| 207 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 237 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 208 | <div class="panel-head"> | 238 | <div class="panel-head"> | ||
| 209 | - | <label for="refs-filter">ref></label> | 239 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 210 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 240 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 211 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 241 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 242 | + | <kbd>esc</kbd> | |||
| 243 | + | </button> | |||
| 212 | </div> | 244 | </div> | ||
| 213 | <div class="panel-cols" data-picklist> | 245 | <div class="panel-cols" data-picklist> | ||
| 214 | <section aria-label="Branches"> | 246 | <section aria-label="Branches"> | ||
| 215 | - | <h2>branches <span class="mute">2</span> | 247 | + | <h2>Branches <span class="count">2</span> |
| 216 | </h2> | 248 | </h2> | ||
| 217 | <ul> | 249 | <ul> | ||
| 218 | <li> | 250 | <li> | ||
| @@ -226,7 +258,7 @@ | |||||
| 226 | </ul> | 258 | </ul> | ||
| 227 | </section> | 259 | </section> | ||
| 228 | <section aria-label="Tags"> | 260 | <section aria-label="Tags"> | ||
| 229 | - | <h2>tags <span class="mute">1</span> | 261 | + | <h2>Tags <span class="count">1</span> |
| 230 | </h2> | 262 | </h2> | ||
| 231 | <ul> | 263 | <ul> | ||
| 232 | <li> | 264 | <li> | ||
| @@ -237,110 +269,130 @@ | |||||
| 237 | </section> | 269 | </section> | ||
| 238 | </div> | 270 | </div> | ||
| 239 | </dialog> | 271 | </dialog> | ||
| 240 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 272 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 241 | <div class="panel-head"> | 273 | <div class="panel-head"> | ||
| 242 | - | <label for="cmd-q" class="hi">:</label> | 274 | + | <label for="cmd-q" class="sr">go to</label> |
| 243 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 275 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 244 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 276 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 245 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 277 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 246 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 278 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 247 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 279 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 248 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 280 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 249 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 281 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 250 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 282 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 283 | + | <kbd>esc</kbd> | |||
| 284 | + | </button> | |||
| 251 | </div> | 285 | </div> | ||
| 252 | - | <ul id="cmd-list" class="cmd-list"> | 286 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 253 | <li data-k="go home" data-cursor> | 287 | <li data-k="go home" data-cursor> | ||
| 254 | <a href="/" tabindex="-1"> | 288 | <a href="/" tabindex="-1"> | ||
| 255 | - | <span class="k">go</span>home</a> | 289 | + | <span class="lbl">home</span> |
| 290 | + | <span class="k">go</span> | |||
| 291 | + | </a> | |||
| 256 | </li> | 292 | </li> | ||
| 257 | <li data-k="section summary"> | 293 | <li data-k="section summary"> | ||
| 258 | <a href="/files" tabindex="-1"> | 294 | <a href="/files" tabindex="-1"> | ||
| 259 | - | <span class="k">section</span>summary</a> | 295 | + | <span class="lbl">summary</span> |
| 296 | + | <span class="k">section</span> | |||
| 297 | + | </a> | |||
| 260 | </li> | 298 | </li> | ||
| 261 | - | <li data-k="section tree"> | 299 | + | <li data-k="section files"> |
| 262 | <a href="/files/tree/main" tabindex="-1"> | 300 | <a href="/files/tree/main" tabindex="-1"> | ||
| 263 | - | <span class="k">section</span>tree</a> | 301 | + | <span class="lbl">files</span> |
| 302 | + | <span class="k">section</span> | |||
| 303 | + | </a> | |||
| 264 | </li> | 304 | </li> | ||
| 265 | <li data-k="section commits"> | 305 | <li data-k="section commits"> | ||
| 266 | <a href="/files/commits/main" tabindex="-1"> | 306 | <a href="/files/commits/main" tabindex="-1"> | ||
| 267 | - | <span class="k">section</span>commits</a> | 307 | + | <span class="lbl">commits</span> |
| 308 | + | <span class="k">section</span> | |||
| 309 | + | </a> | |||
| 268 | </li> | 310 | </li> | ||
| 269 | - | <li data-k="section refs"> | 311 | + | <li data-k="section branches & tags"> |
| 270 | <a href="/files/refs" tabindex="-1"> | 312 | <a href="/files/refs" tabindex="-1"> | ||
| 271 | - | <span class="k">section</span>refs</a> | 313 | + | <span class="lbl">branches & tags</span> |
| 314 | + | <span class="k">section</span> | |||
| 315 | + | </a> | |||
| 272 | </li> | 316 | </li> | ||
| 273 | <li data-k="section merge requests"> | 317 | <li data-k="section merge requests"> | ||
| 274 | <a href="/files/merge-requests" tabindex="-1"> | 318 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 275 | - | <span class="k">section</span>merge requests</a> | 319 | + | <span class="lbl">merge requests</span> |
| 320 | + | <span class="k">section</span> | |||
| 321 | + | </a> | |||
| 276 | </li> | 322 | </li> | ||
| 277 | <li data-k="branch feature/x"> | 323 | <li data-k="branch feature/x"> | ||
| 278 | <a href="/files/tree/feature/x" tabindex="-1"> | 324 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 279 | - | <span class="k">branch</span>feature/x</a> | 325 | + | <span class="lbl">feature/x</span> |
| 326 | + | <span class="k">branch</span> | |||
| 327 | + | </a> | |||
| 280 | </li> | 328 | </li> | ||
| 281 | <li data-k="branch main"> | 329 | <li data-k="branch main"> | ||
| 282 | <a href="/files/tree/main" tabindex="-1"> | 330 | <a href="/files/tree/main" tabindex="-1"> | ||
| 283 | - | <span class="k">branch</span>main</a> | 331 | + | <span class="lbl">main</span> |
| 332 | + | <span class="k">branch</span> | |||
| 333 | + | </a> | |||
| 284 | </li> | 334 | </li> | ||
| 285 | <li data-k="tag v1.0"> | 335 | <li data-k="tag v1.0"> | ||
| 286 | <a href="/files/tree/v1.0" tabindex="-1"> | 336 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 287 | - | <span class="k">tag</span>v1.0</a> | 337 | + | <span class="lbl">v1.0</span> |
| 338 | + | <span class="k">tag</span> | |||
| 339 | + | </a> | |||
| 288 | </li> | 340 | </li> | ||
| 289 | <li data-k="file x/y.txt"> | 341 | <li data-k="file x/y.txt"> | ||
| 290 | <a href="/files/blob/feature/x/x/y.txt" tabindex="-1"> | 342 | <a href="/files/blob/feature/x/x/y.txt" tabindex="-1"> | ||
| 291 | - | <span class="k">file</span>x/y.txt</a> | 343 | + | <span class="lbl">x/y.txt</span> |
| 344 | + | <span class="k">file</span> | |||
| 345 | + | </a> | |||
| 292 | </li> | 346 | </li> | ||
| 293 | </ul> | 347 | </ul> | ||
| 348 | + | <p class="panel-foot"> | |||
| 349 | + | <span> | |||
| 350 | + | <kbd>↑</kbd> | |||
| 351 | + | <kbd>↓</kbd> move</span> | |||
| 352 | + | <span> | |||
| 353 | + | <kbd>enter</kbd> open</span> | |||
| 354 | + | <span> | |||
| 355 | + | <kbd>esc</kbd> close</span> | |||
| 356 | + | </p> | |||
| 294 | </dialog> | 357 | </dialog> | ||
| 295 | - | <footer class="bar status" aria-label="Status"> | 358 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 296 | - | <span class="mode">tree</span> | |||
| 297 | - | <span class="st-ctx">tree · 1 entry</span> | |||
| 298 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 299 | - | <span class="hi">:</span> go to…</button> | |||
| 300 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 301 | - | </span> | |||
| 302 | - | <span class="st-hints">j/k move · enter open · / filter · b ref · ? help</span> | |||
| 303 | - | <span class="st-src"> | |||
| 304 | - | <span class="st-ver">ermite devel · </span> | |||
| 305 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 306 | - | </span> | |||
| 307 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 308 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/1</span> | |||
| 309 | - | </footer> | |||
| 310 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 311 | if (q('dialog[open]').count) return; | 359 | if (q('dialog[open]').count) return; | ||
| 312 | let rows = q('[data-rows] > :not([hidden])').arr(); | 360 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 313 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 361 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 314 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 362 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 315 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 363 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 316 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 364 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 365 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 366 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 317 | })(1); | 367 | })(1); | ||
| 318 | - | k-k from:body -> ((d) => { | 368 | + | k-k from:body -> (async (d) => { |
| 319 | if (q('dialog[open]').count) return; | 369 | if (q('dialog[open]').count) return; | ||
| 320 | let rows = q('[data-rows] > :not([hidden])').arr(); | 370 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 321 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 371 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 322 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 372 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 323 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 373 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 324 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 374 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 375 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 376 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 325 | })(-1); | 377 | })(-1); | ||
| 326 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 378 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 327 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 379 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 328 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 380 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 329 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 381 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 330 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 382 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 331 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 383 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 332 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 384 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 333 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 385 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 334 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 386 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 335 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 387 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 336 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 388 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 337 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 389 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 338 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 390 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 339 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 391 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 340 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 392 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 341 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 393 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 342 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 394 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 343 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 395 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 344 | k-next from:body -> { | 396 | k-next from:body -> { | ||
| 345 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 397 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 346 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 398 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/testdata/web/tree-subdir.html +170 -114
| @@ -6,6 +6,7 @@ | |||||
| 6 | <meta name="color-scheme" content="light dark"> | 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>docs · files · example.test</title> | 7 | <title>docs · files · example.test</title> | ||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | + | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |||
| 9 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 10 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 11 | </script> | 12 | </script> | ||
| @@ -16,12 +17,15 @@ | |||||
| 16 | <script src="/-/static/js/ermite.HASH.js" defer> | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 17 | </script> | 18 | </script> | ||
| 18 | </head> | 19 | </head> | ||
| 19 | - | <body data-ref="main" data-sha="SHA40" data-sel=""> | 20 | + | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav=""> |
| 20 | <a class="skip" href="#main">skip to content</a> | 21 | <a class="skip" href="#main">skip to content</a> | ||
| 21 | - | <header class="bar top"> | 22 | + | <header class="hdr"> |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | 23 | + | <div class="hdr-top"> |
| 23 | - | <span class="fade" aria-hidden="true"> | 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |||
| 24 | </span> | 26 | </span> | ||
| 27 | + | <span>example.test</span> | |||
| 28 | + | </a> | |||
| 25 | <nav class="crumbs" aria-label="Path"> | 29 | <nav class="crumbs" aria-label="Path"> | ||
| 26 | <ol> | 30 | <ol> | ||
| 27 | <li> | 31 | <li> | ||
| @@ -38,33 +42,52 @@ | |||||
| 38 | </li> | 42 | </li> | ||
| 39 | </ol> | 43 | </ol> | ||
| 40 | </nav> | 44 | </nav> | ||
| 41 | - | <nav class="secs" aria-label="Sections"> | 45 | + | <div class="hdr-acts"> |
| 46 | + | <button type="button" class="btn ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch branch or tag (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))"> | |||
| 47 | + | <span class="ico i-branch" aria-hidden="true"> | |||
| 48 | + | </span> | |||
| 49 | + | <span class="ref-name">main</span> | |||
| 50 | + | <span class="sha">SHA7</span> | |||
| 51 | + | </button> | |||
| 52 | + | <button type="button" class="btn search" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 53 | + | <span class="ico i-search" aria-hidden="true"> | |||
| 54 | + | </span> | |||
| 55 | + | <span class="search-label">Go to…</span> | |||
| 56 | + | <kbd>ctrl k</kbd> | |||
| 57 | + | </button> | |||
| 58 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 59 | + | </div> | |||
| 60 | + | </div> | |||
| 61 | + | <nav class="tabs" aria-label="Sections"> | |||
| 42 | <ul> | 62 | <ul> | ||
| 43 | <li> | 63 | <li> | ||
| 44 | - | <a href="/files" data-key="s" aria-keyshortcuts="g s">summary</a> | 64 | + | <a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd> |
| 65 | + | </a> | |||
| 45 | </li> | 66 | </li> | ||
| 46 | <li> | 67 | <li> | ||
| 47 | - | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="g t" aria-current="page">tree</a> | 68 | + | <a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd> |
| 69 | + | </a> | |||
| 48 | </li> | 70 | </li> | ||
| 49 | <li> | 71 | <li> | ||
| 50 | - | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="g c">commits</a> | 72 | + | <a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd> |
| 73 | + | </a> | |||
| 51 | </li> | 74 | </li> | ||
| 52 | <li> | 75 | <li> | ||
| 53 | - | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | 76 | + | <a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches & tags<kbd aria-hidden="true">r</kbd> |
| 77 | + | </a> | |||
| 54 | </li> | 78 | </li> | ||
| 55 | <li> | 79 | <li> | ||
| 56 | - | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | 80 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd> |
| 81 | + | </a> | |||
| 57 | </li> | 82 | </li> | ||
| 58 | </ul> | 83 | </ul> | ||
| 59 | </nav> | 84 | </nav> | ||
| 60 | - | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |||
| 61 | - | </button> | |||
| 62 | </header> | 85 | </header> | ||
| 63 | <div class="work"> | 86 | <div class="work"> | ||
| 64 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 87 | + | <main id="main" tabindex="-1"> |
| 65 | <div class="filter"> | 88 | <div class="filter"> | ||
| 66 | - | <label for="tree-filter">filter></label> | 89 | + | <label for="tree-filter" class="sr">filter</label> |
| 67 | - | <input id="tree-filter" type="search" data-filter placeholder="this directory ( / )" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')"> | 90 | + | <input id="tree-filter" type="search" data-filter placeholder="Filter this directory…" autocomplete="off" hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')"> |
| 68 | </div> | 91 | </div> | ||
| 69 | <table class="list tree" aria-label="Entries"> | 92 | <table class="list tree" aria-label="Entries"> | ||
| 70 | <thead> | 93 | <thead> | ||
| @@ -115,79 +138,82 @@ | |||||
| 115 | </tr> | 138 | </tr> | ||
| 116 | </tbody> | 139 | </tbody> | ||
| 117 | </table> | 140 | </table> | ||
| 118 | - | <h2 class="rule-h"> | 141 | + | <section class="card readme" aria-labelledby="readme-h"> |
| 119 | - | <span>README.md</span> | 142 | + | <h2 class="card-h" id="readme-h"> |
| 120 | - | </h2> | 143 | + | <span class="ico i-file" aria-hidden="true"> |
| 144 | + | </span> README.md</h2> | |||
| 121 | <article class="prose" aria-label="README.md"> | 145 | <article class="prose" aria-label="README.md"> | ||
| 122 | <h1 id="md-docs">Docs</h1> | 146 | <h1 id="md-docs">Docs</h1> | ||
| 123 | <p>Back to <a href="/files/blob/main/docs/guide.md">the guide</a> and <a href="/files/blob/main/main.go">root</a>.</p> | 147 | <p>Back to <a href="/files/blob/main/docs/guide.md">the guide</a> and <a href="/files/blob/main/main.go">root</a>.</p> | ||
| 124 | </article> | 148 | </article> | ||
| 149 | + | </section> | |||
| 125 | </main> | 150 | </main> | ||
| 126 | </div> | 151 | </div> | ||
| 127 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 152 | + | <footer class="foot"> |
| 153 | + | <span>ermite devel</span> | |||
| 154 | + | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 155 | + | <span class="foot-keys"> | |||
| 156 | + | <kbd>?</kbd> keyboard shortcuts</span> | |||
| 157 | + | </footer> | |||
| 158 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 159 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"> | |||
| 160 | + | </span> | |||
| 161 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"> | |||
| 162 | + | </span> | |||
| 163 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"> | |||
| 164 | + | </span> | |||
| 165 | + | </div> | |||
| 166 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |||
| 128 | <div class="panel-head"> | 167 | <div class="panel-head"> | ||
| 129 | - | <span>keys</span> | 168 | + | <h2>Keyboard shortcuts</h2> |
| 130 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 169 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 170 | + | <kbd>esc</kbd> close</button> | |||
| 131 | </div> | 171 | </div> | ||
| 132 | <dl class="keys"> | 172 | <dl class="keys"> | ||
| 133 | <div> | 173 | <div> | ||
| 134 | <dt> | 174 | <dt> | ||
| 135 | - | <kbd>j</kbd> | 175 | + | <kbd>ctrl k</kbd> |
| 136 | - | <kbd>k</kbd> | 176 | + | <kbd>:</kbd> |
| 137 | </dt> | 177 | </dt> | ||
| 138 | - | <dd>move cursor down / up</dd> | 178 | + | <dd>go to a section, branch, tag, file or repository</dd> |
| 139 | </div> | 179 | </div> | ||
| 140 | <div> | 180 | <div> | ||
| 141 | <dt> | 181 | <dt> | ||
| 142 | - | <kbd>enter</kbd> | 182 | + | <kbd>?</kbd> |
| 143 | </dt> | 183 | </dt> | ||
| 144 | - | <dd>open row under cursor</dd> | 184 | + | <dd>this list</dd> |
| 145 | </div> | 185 | </div> | ||
| 146 | <div> | 186 | <div> | ||
| 147 | <dt> | 187 | <dt> | ||
| 148 | - | <kbd>g</kbd> | 188 | + | <kbd>j</kbd> |
| 149 | - | <kbd>g</kbd> / <kbd>G</kbd> | 189 | + | <kbd>k</kbd> |
| 150 | </dt> | 190 | </dt> | ||
| 151 | - | <dd>first / last row</dd> | 191 | + | <dd>move the row cursor down / up</dd> |
| 152 | </div> | 192 | </div> | ||
| 153 | <div> | 193 | <div> | ||
| 154 | <dt> | 194 | <dt> | ||
| 155 | - | <kbd>/</kbd> | 195 | + | <kbd>enter</kbd> |
| 156 | </dt> | 196 | </dt> | ||
| 157 | - | <dd>focus the filter</dd> | 197 | + | <dd>open the row under the cursor</dd> |
| 158 | </div> | 198 | </div> | ||
| 159 | <div> | 199 | <div> | ||
| 160 | <dt> | 200 | <dt> | ||
| 161 | - | <kbd>g h</kbd> | 201 | + | <kbd>g g</kbd> |
| 162 | - | <kbd>g t</kbd> | 202 | + | <kbd>G</kbd> |
| 163 | </dt> | 203 | </dt> | ||
| 164 | - | <dd>go home / tree</dd> | 204 | + | <dd>first / last row</dd> |
| 165 | </div> | 205 | </div> | ||
| 166 | <div> | 206 | <div> | ||
| 167 | <dt> | 207 | <dt> | ||
| 168 | - | <kbd>g c</kbd> | 208 | + | <kbd>/</kbd> |
| 169 | - | <kbd>g r</kbd> | |||
| 170 | </dt> | 209 | </dt> | ||
| 171 | - | <dd>go commits / refs</dd> | 210 | + | <dd>filter the list</dd> |
| 172 | </div> | 211 | </div> | ||
| 173 | <div> | 212 | <div> | ||
| 174 | <dt> | 213 | <dt> | ||
| 175 | <kbd>b</kbd> | 214 | <kbd>b</kbd> | ||
| 176 | </dt> | 215 | </dt> | ||
| 177 | - | <dd>switch ref (branches, tags)</dd> | 216 | + | <dd>switch branch or tag</dd> |
| 178 | - | </div> | |||
| 179 | - | <div> | |||
| 180 | - | <dt> | |||
| 181 | - | <kbd>:</kbd> | |||
| 182 | - | <kbd>f</kbd> | |||
| 183 | - | </dt> | |||
| 184 | - | <dd>go to: section, ref, file, repo</dd> | |||
| 185 | - | </div> | |||
| 186 | - | <div> | |||
| 187 | - | <dt> | |||
| 188 | - | <kbd>y</kbd> | |||
| 189 | - | </dt> | |||
| 190 | - | <dd>permalink: ref becomes the full sha</dd> | |||
| 191 | </div> | 217 | </div> | ||
| 192 | <div> | 218 | <div> | ||
| 193 | <dt> | 219 | <dt> | ||
| @@ -195,15 +221,21 @@ | |||||
| 195 | <kbd>t</kbd> | 221 | <kbd>t</kbd> | ||
| 196 | <kbd>c</kbd> | 222 | <kbd>c</kbd> | ||
| 197 | <kbd>r</kbd> | 223 | <kbd>r</kbd> | ||
| 224 | + | <kbd>m</kbd> | |||
| 198 | </dt> | 225 | </dt> | ||
| 199 | - | <dd>repo sections (when shown)</dd> | 226 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> |
| 200 | </div> | 227 | </div> | ||
| 201 | <div> | 228 | <div> | ||
| 202 | <dt> | 229 | <dt> | ||
| 203 | - | <kbd>m</kbd> | 230 | + | <kbd>g h</kbd> |
| 204 | - | <kbd>g m</kbd> | |||
| 205 | </dt> | 231 | </dt> | ||
| 206 | - | <dd>merge requests (when shown)</dd> | 232 | + | <dd>home</dd> |
| 233 | + | </div> | |||
| 234 | + | <div> | |||
| 235 | + | <dt> | |||
| 236 | + | <kbd>y</kbd> | |||
| 237 | + | </dt> | |||
| 238 | + | <dd>copy a permalink pinned to the commit</dd> | |||
| 207 | </div> | 239 | </div> | ||
| 208 | <div> | 240 | <div> | ||
| 209 | <dt> | 241 | <dt> | ||
| @@ -223,19 +255,21 @@ | |||||
| 223 | <dt> | 255 | <dt> | ||
| 224 | <kbd>esc</kbd> | 256 | <kbd>esc</kbd> | ||
| 225 | </dt> | 257 | </dt> | ||
| 226 | - | <dd>close panel</dd> | 258 | + | <dd>close a dialog, leave a field</dd> |
| 227 | </div> | 259 | </div> | ||
| 228 | </dl> | 260 | </dl> | ||
| 229 | </dialog> | 261 | </dialog> | ||
| 230 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 262 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 231 | <div class="panel-head"> | 263 | <div class="panel-head"> | ||
| 232 | - | <label for="refs-filter">ref></label> | 264 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 233 | - | <input id="refs-filter" type="search" placeholder="filter branches and tags" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | 265 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> |
| 234 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 266 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 267 | + | <kbd>esc</kbd> | |||
| 268 | + | </button> | |||
| 235 | </div> | 269 | </div> | ||
| 236 | <div class="panel-cols" data-picklist> | 270 | <div class="panel-cols" data-picklist> | ||
| 237 | <section aria-label="Branches"> | 271 | <section aria-label="Branches"> | ||
| 238 | - | <h2>branches <span class="mute">2</span> | 272 | + | <h2>Branches <span class="count">2</span> |
| 239 | </h2> | 273 | </h2> | ||
| 240 | <ul> | 274 | <ul> | ||
| 241 | <li> | 275 | <li> | ||
| @@ -249,7 +283,7 @@ | |||||
| 249 | </ul> | 283 | </ul> | ||
| 250 | </section> | 284 | </section> | ||
| 251 | <section aria-label="Tags"> | 285 | <section aria-label="Tags"> | ||
| 252 | - | <h2>tags <span class="mute">1</span> | 286 | + | <h2>Tags <span class="count">1</span> |
| 253 | </h2> | 287 | </h2> | ||
| 254 | <ul> | 288 | <ul> | ||
| 255 | <li> | 289 | <li> | ||
| @@ -260,114 +294,136 @@ | |||||
| 260 | </section> | 294 | </section> | ||
| 261 | </div> | 295 | </div> | ||
| 262 | </dialog> | 296 | </dialog> | ||
| 263 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 297 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 264 | <div class="panel-head"> | 298 | <div class="panel-head"> | ||
| 265 | - | <label for="cmd-q" class="hi">:</label> | 299 | + | <label for="cmd-q" class="sr">go to</label> |
| 266 | - | <input id="cmd-q" type="search" placeholder="section, ref, file or repo" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | 300 | + | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); |
| 267 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | 301 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | ||
| 268 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | 302 | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | ||
| 269 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | 303 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | ||
| 270 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | 304 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | ||
| 271 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 305 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 272 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | 306 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | ||
| 273 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 307 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> |
| 308 | + | <kbd>esc</kbd> | |||
| 309 | + | </button> | |||
| 274 | </div> | 310 | </div> | ||
| 275 | - | <ul id="cmd-list" class="cmd-list"> | 311 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 276 | <li data-k="go home" data-cursor> | 312 | <li data-k="go home" data-cursor> | ||
| 277 | <a href="/" tabindex="-1"> | 313 | <a href="/" tabindex="-1"> | ||
| 278 | - | <span class="k">go</span>home</a> | 314 | + | <span class="lbl">home</span> |
| 315 | + | <span class="k">go</span> | |||
| 316 | + | </a> | |||
| 279 | </li> | 317 | </li> | ||
| 280 | <li data-k="section summary"> | 318 | <li data-k="section summary"> | ||
| 281 | <a href="/files" tabindex="-1"> | 319 | <a href="/files" tabindex="-1"> | ||
| 282 | - | <span class="k">section</span>summary</a> | 320 | + | <span class="lbl">summary</span> |
| 321 | + | <span class="k">section</span> | |||
| 322 | + | </a> | |||
| 283 | </li> | 323 | </li> | ||
| 284 | - | <li data-k="section tree"> | 324 | + | <li data-k="section files"> |
| 285 | <a href="/files/tree/main" tabindex="-1"> | 325 | <a href="/files/tree/main" tabindex="-1"> | ||
| 286 | - | <span class="k">section</span>tree</a> | 326 | + | <span class="lbl">files</span> |
| 327 | + | <span class="k">section</span> | |||
| 328 | + | </a> | |||
| 287 | </li> | 329 | </li> | ||
| 288 | <li data-k="section commits"> | 330 | <li data-k="section commits"> | ||
| 289 | <a href="/files/commits/main" tabindex="-1"> | 331 | <a href="/files/commits/main" tabindex="-1"> | ||
| 290 | - | <span class="k">section</span>commits</a> | 332 | + | <span class="lbl">commits</span> |
| 333 | + | <span class="k">section</span> | |||
| 334 | + | </a> | |||
| 291 | </li> | 335 | </li> | ||
| 292 | - | <li data-k="section refs"> | 336 | + | <li data-k="section branches & tags"> |
| 293 | <a href="/files/refs" tabindex="-1"> | 337 | <a href="/files/refs" tabindex="-1"> | ||
| 294 | - | <span class="k">section</span>refs</a> | 338 | + | <span class="lbl">branches & tags</span> |
| 339 | + | <span class="k">section</span> | |||
| 340 | + | </a> | |||
| 295 | </li> | 341 | </li> | ||
| 296 | <li data-k="section merge requests"> | 342 | <li data-k="section merge requests"> | ||
| 297 | <a href="/files/merge-requests" tabindex="-1"> | 343 | <a href="/files/merge-requests" tabindex="-1"> | ||
| 298 | - | <span class="k">section</span>merge requests</a> | 344 | + | <span class="lbl">merge requests</span> |
| 345 | + | <span class="k">section</span> | |||
| 346 | + | </a> | |||
| 299 | </li> | 347 | </li> | ||
| 300 | <li data-k="branch feature/x"> | 348 | <li data-k="branch feature/x"> | ||
| 301 | <a href="/files/tree/feature/x" tabindex="-1"> | 349 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 302 | - | <span class="k">branch</span>feature/x</a> | 350 | + | <span class="lbl">feature/x</span> |
| 351 | + | <span class="k">branch</span> | |||
| 352 | + | </a> | |||
| 303 | </li> | 353 | </li> | ||
| 304 | <li data-k="branch main"> | 354 | <li data-k="branch main"> | ||
| 305 | <a href="/files/tree/main" tabindex="-1"> | 355 | <a href="/files/tree/main" tabindex="-1"> | ||
| 306 | - | <span class="k">branch</span>main</a> | 356 | + | <span class="lbl">main</span> |
| 357 | + | <span class="k">branch</span> | |||
| 358 | + | </a> | |||
| 307 | </li> | 359 | </li> | ||
| 308 | <li data-k="tag v1.0"> | 360 | <li data-k="tag v1.0"> | ||
| 309 | <a href="/files/tree/v1.0" tabindex="-1"> | 361 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 310 | - | <span class="k">tag</span>v1.0</a> | 362 | + | <span class="lbl">v1.0</span> |
| 363 | + | <span class="k">tag</span> | |||
| 364 | + | </a> | |||
| 311 | </li> | 365 | </li> | ||
| 312 | <li data-k="file docs/readme.md"> | 366 | <li data-k="file docs/readme.md"> | ||
| 313 | <a href="/files/blob/main/docs/README.md" tabindex="-1"> | 367 | <a href="/files/blob/main/docs/README.md" tabindex="-1"> | ||
| 314 | - | <span class="k">file</span>docs/README.md</a> | 368 | + | <span class="lbl">docs/README.md</span> |
| 369 | + | <span class="k">file</span> | |||
| 370 | + | </a> | |||
| 315 | </li> | 371 | </li> | ||
| 316 | <li data-k="file docs/guide.md"> | 372 | <li data-k="file docs/guide.md"> | ||
| 317 | <a href="/files/blob/main/docs/guide.md" tabindex="-1"> | 373 | <a href="/files/blob/main/docs/guide.md" tabindex="-1"> | ||
| 318 | - | <span class="k">file</span>docs/guide.md</a> | 374 | + | <span class="lbl">docs/guide.md</span> |
| 375 | + | <span class="k">file</span> | |||
| 376 | + | </a> | |||
| 319 | </li> | 377 | </li> | ||
| 320 | </ul> | 378 | </ul> | ||
| 379 | + | <p class="panel-foot"> | |||
| 380 | + | <span> | |||
| 381 | + | <kbd>↑</kbd> | |||
| 382 | + | <kbd>↓</kbd> move</span> | |||
| 383 | + | <span> | |||
| 384 | + | <kbd>enter</kbd> open</span> | |||
| 385 | + | <span> | |||
| 386 | + | <kbd>esc</kbd> close</span> | |||
| 387 | + | </p> | |||
| 321 | </dialog> | 388 | </dialog> | ||
| 322 | - | <footer class="bar status" aria-label="Status"> | 389 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { |
| 323 | - | <span class="mode">tree</span> | |||
| 324 | - | <span class="st-ctx">tree · 2 entries</span> | |||
| 325 | - | <button type="button" class="st-cmd" aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts=":" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))"> | |||
| 326 | - | <span class="hi">:</span> go to…</button> | |||
| 327 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"> | |||
| 328 | - | </span> | |||
| 329 | - | <span class="st-hints">j/k move · enter open · / filter · b ref · ? help</span> | |||
| 330 | - | <span class="st-src"> | |||
| 331 | - | <span class="st-ver">ermite devel · </span> | |||
| 332 | - | <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 333 | - | </span> | |||
| 334 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 335 | - | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/2</span> | |||
| 336 | - | </footer> | |||
| 337 | - | <div id="keys" hidden hx-on="k-j from:body -> ((d) => { | |||
| 338 | if (q('dialog[open]').count) return; | 390 | if (q('dialog[open]').count) return; | ||
| 339 | let rows = q('[data-rows] > :not([hidden])').arr(); | 391 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 340 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 392 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 341 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 393 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 342 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 394 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 343 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 395 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 396 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 397 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 344 | })(1); | 398 | })(1); | ||
| 345 | - | k-k from:body -> ((d) => { | 399 | + | k-k from:body -> (async (d) => { |
| 346 | if (q('dialog[open]').count) return; | 400 | if (q('dialog[open]').count) return; | ||
| 347 | let rows = q('[data-rows] > :not([hidden])').arr(); | 401 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 348 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 402 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 349 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 403 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 350 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 404 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 351 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 405 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 406 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 407 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 352 | })(-1); | 408 | })(-1); | ||
| 353 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 409 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 354 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 410 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 355 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 411 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 356 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 412 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 357 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 413 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 358 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 414 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 359 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 415 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 360 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 416 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 361 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 417 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 362 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 418 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 363 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 419 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 364 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 420 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 365 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 421 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 366 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 422 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 367 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 423 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 368 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 424 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 369 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 425 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 370 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 426 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 371 | k-next from:body -> { | 427 | k-next from:body -> { | ||
| 372 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 428 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 373 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 429 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M cmd/ermite/web_commits_test.go +6 -6
| @@ -122,7 +122,7 @@ func (h *harness) longRepo(name string) { | |||||
| 122 | h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/"+name+".git"), "main") | 122 | h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/"+name+".git"), "main") | ||
| 123 | } | 123 | } | ||
| 124 | 124 | ||||
| 125 | - | var reOlder = regexp.MustCompile(`<a href="([^"]+)" rel="next">`) | 125 | + | var reOlder = regexp.MustCompile(`<a class="btn" href="([^"]+)" rel="next">`) |
| 126 | 126 | ||||
| 127 | // unescape undoes the HTML escaping of an attribute value. | 127 | // unescape undoes the HTML escaping of an attribute value. | ||
| 128 | func unescape(s string) string { return strings.ReplaceAll(s, "&", "&") } | 128 | func unescape(s string) string { return strings.ReplaceAll(s, "&", "&") } | ||
| @@ -174,12 +174,12 @@ func TestWebCommits(t *testing.T) { | |||||
| 174 | assert.Equal(t, 50, strings.Count(first, `class="c-sha mute"`), "a page lists 50 commits") | 174 | assert.Equal(t, 50, strings.Count(first, `class="c-sha mute"`), "a page lists 50 commits") | ||
| 175 | assert.Contains(t, first, "commit number 51") | 175 | assert.Contains(t, first, "commit number 51") | ||
| 176 | assert.NotContains(t, first, "commit number 1<", "the oldest are on the next page") | 176 | assert.NotContains(t, first, "commit number 1<", "the oldest are on the next page") | ||
| 177 | - | assert.Contains(t, first, "<span class=\"mute\">← newest</span>") | 177 | + | assert.Contains(t, first, `<span class="btn" aria-disabled="true">← Newest</span>`) |
| 178 | second := unescape(reOlder.FindStringSubmatch(first)[1]) | 178 | second := unescape(reOlder.FindStringSubmatch(first)[1]) | ||
| 179 | page2 := get(second, 200) | 179 | page2 := get(second, 200) | ||
| 180 | assert.Equal(t, 2, strings.Count(page2, `class="c-sha mute"`)) | 180 | assert.Equal(t, 2, strings.Count(page2, `class="c-sha mute"`)) | ||
| 181 | - | assert.Contains(t, page2, `href="/long/commits/main">← newest</a>`) | 181 | + | assert.Contains(t, page2, `href="/long/commits/main">← Newest</a>`) |
| 182 | - | assert.Contains(t, page2, "<span class=\"mute\">older →</span>") | 182 | + | assert.Contains(t, page2, `<span class="btn" aria-disabled="true">Older →</span>`) |
| 183 | get("/long/commits/main?page=garbage", 404) | 183 | get("/long/commits/main?page=garbage", 404) | ||
| 184 | }) | 184 | }) | ||
| 185 | 185 | ||||
| @@ -196,8 +196,8 @@ func TestWebCommits(t *testing.T) { | |||||
| 196 | assert.Contains(t, get("/diffs/commits/v1.0.0", 200), "modify a file") | 196 | assert.Contains(t, get("/diffs/commits/v1.0.0", 200), "modify a file") | ||
| 197 | assert.Contains(t, get("/diffs/commits/side", 200), "work on the side") | 197 | assert.Contains(t, get("/diffs/commits/side", 200), "work on the side") | ||
| 198 | assert.Contains(t, get("/diffs/commits/"+f.modify[:10], 200), "modify a file") | 198 | assert.Contains(t, get("/diffs/commits/"+f.modify[:10], 200), "modify a file") | ||
| 199 | - | assert.Contains(t, get("/diffs/commits/main", 200), `[tag: v1.0.0]`) | 199 | + | assert.Contains(t, get("/diffs/commits/main", 200), `<span class="pill tag"><span class="sr">tag </span>v1.0.0</span>`) |
| 200 | - | assert.Contains(t, get("/diffs/commits/main", 200), `(merge)`) | 200 | + | assert.Contains(t, get("/diffs/commits/main", 200), `<span class="pill">merge</span>`) |
| 201 | get("/diffs/commits/nope", 404) | 201 | get("/diffs/commits/nope", 404) | ||
| 202 | get("/diffs/commits/mai", 404) | 202 | get("/diffs/commits/mai", 404) | ||
| 203 | get("/nope/commits/main", 404) | 203 | get("/nope/commits/main", 404) | ||
M cmd/ermite/web_mr_test.go +8 -8
| @@ -141,7 +141,7 @@ func TestWebMergeRequestPagesRender(t *testing.T) { | |||||
| 141 | assert.Contains(t, mr.body, "Finds things.\nSee ", "line breaks are kept") | 141 | assert.Contains(t, mr.body, "Finds things.\nSee ", "line breaks are kept") | ||
| 142 | assert.NotContains(t, mr.body, "<form", "an anonymous visitor gets no form") | 142 | assert.NotContains(t, mr.body, "<form", "an anonymous visitor gets no form") | ||
| 143 | assert.NotContains(t, mr.body, "ermite mr merge", "nor the merge line") | 143 | assert.NotContains(t, mr.body, "ermite mr merge", "nor the merge line") | ||
| 144 | - | assert.NotContains(t, mr.body, "[merge]") | 144 | + | assert.NotContains(t, mr.body, ">Merge</button>") |
| 145 | assert.Contains(t, mr.body, `href="/proj/commit/`+head+`"`, "the commits are in the rail") | 145 | assert.Contains(t, mr.body, `href="/proj/commit/`+head+`"`, "the commits are in the rail") | ||
| 146 | assert.NotContains(t, strings.ToLower(mr.body), "proposal") | 146 | assert.NotContains(t, strings.ToLower(mr.body), "proposal") | ||
| 147 | assertNoInline(t, mr.body) | 147 | assertNoInline(t, mr.body) | ||
| @@ -303,7 +303,7 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||||
| 303 | assert.Equal(t, base+"#c1", res.hdr.Get("Location")) | 303 | assert.Equal(t, base+"#c1", res.hdr.Get("Location")) | ||
| 304 | own = b.get(base, as(token)) | 304 | own = b.get(base, as(token)) | ||
| 305 | assert.Contains(t, own.body, `data-resolved="true"`) | 305 | assert.Contains(t, own.body, `data-resolved="true"`) | ||
| 306 | - | assert.Contains(t, own.body, "[unresolve]") | 306 | + | assert.Contains(t, own.body, ">Unresolve</button>") |
| 307 | assert.Equal(t, http.StatusSeeOther, b.form(base+"/comments/1/resolve", token, url.Values{"resolved": {"false"}}).status) | 307 | assert.Equal(t, http.StatusSeeOther, b.form(base+"/comments/1/resolve", token, url.Values{"resolved": {"false"}}).status) | ||
| 308 | assert.Equal(t, http.StatusBadRequest, b.form(base+"/comments/1/resolve", token, url.Values{"resolved": {"maybe"}}).status) | 308 | assert.Equal(t, http.StatusBadRequest, b.form(base+"/comments/1/resolve", token, url.Values{"resolved": {"maybe"}}).status) | ||
| 309 | assert.Equal(t, 404, b.form(base+"/comments/42/resolve", token, url.Values{"resolved": {"true"}}).status) | 309 | assert.Equal(t, 404, b.form(base+"/comments/42/resolve", token, url.Values{"resolved": {"true"}}).status) | ||
| @@ -312,7 +312,7 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||||
| 312 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/read"`) | 312 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/read"`) | ||
| 313 | res = b.form(base+"/read", token, url.Values{"head_id": {head1}}) | 313 | res = b.form(base+"/read", token, url.Values{"head_id": {head1}}) | ||
| 314 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | 314 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | ||
| 315 | - | assert.NotContains(t, b.get(base, as(token)).body, "[mark read]", "the head is the one that was read") | 315 | + | assert.NotContains(t, b.get(base, as(token)).body, ">Mark read</button>", "the head is the one that was read") |
| 316 | r.writeFile(r.wt, "extra.txt", []byte("extra\n")) | 316 | r.writeFile(r.wt, "extra.txt", []byte("extra\n")) | ||
| 317 | r.mustGit(r.wt, "push", "-q", r.url, "feat/search") | 317 | r.mustGit(r.wt, "push", "-q", r.url, "feat/search") | ||
| 318 | head2 := r.headOf("feat/search") | 318 | head2 := r.headOf("feat/search") | ||
| @@ -321,7 +321,7 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||||
| 321 | assert.Contains(t, all.body, `href="/proj/merge-requests/1/since"`) | 321 | assert.Contains(t, all.body, `href="/proj/merge-requests/1/since"`) | ||
| 322 | assert.Contains(t, all.body, "since your last read") | 322 | assert.Contains(t, all.body, "since your last read") | ||
| 323 | assert.Contains(t, all.body, "<li data-new>", "the commit pushed since is marked") | 323 | assert.Contains(t, all.body, "<li data-new>", "the commit pushed since is marked") | ||
| 324 | - | assert.Contains(t, all.body, "[mark read]") | 324 | + | assert.Contains(t, all.body, ">Mark read</button>") |
| 325 | assert.Contains(t, all.body, "outdated", "the notes were written on the earlier head") | 325 | assert.Contains(t, all.body, "outdated", "the notes were written on the earlier head") | ||
| 326 | since := b.get(base+"/since", as(token)) | 326 | since := b.get(base+"/since", as(token)) | ||
| 327 | require.Equal(t, 200, since.status, since.body) | 327 | require.Equal(t, 200, since.status, since.body) | ||
| @@ -356,14 +356,14 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||||
| 356 | assert.Contains(t, done.body, "fast-forward") | 356 | assert.Contains(t, done.body, "fast-forward") | ||
| 357 | assert.Contains(t, done.body, `href="/proj/commit/`+head2+`"`) | 357 | assert.Contains(t, done.body, `href="/proj/commit/`+head2+`"`) | ||
| 358 | assert.NotContains(t, done.body, "<form") | 358 | assert.NotContains(t, done.body, "<form") | ||
| 359 | - | assert.NotContains(t, b.get(base, as(token)).body, "[merge]", "nothing to merge any more") | 359 | + | assert.NotContains(t, b.get(base, as(token)).body, ">Merge</button>", "nothing to merge any more") |
| 360 | // Writes on a merged request are refused with a page. | 360 | // Writes on a merged request are refused with a page. | ||
| 361 | late := b.form(base+"/comments", token, url.Values{"text": {"too late"}}) | 361 | late := b.form(base+"/comments", token, url.Values{"text": {"too late"}}) | ||
| 362 | assert.Equal(t, http.StatusConflict, late.status) | 362 | assert.Equal(t, http.StatusConflict, late.status) | ||
| 363 | assert.Equal(t, http.StatusConflict, b.form(base+"/close", token, nil).status) | 363 | assert.Equal(t, http.StatusConflict, b.form(base+"/close", token, nil).status) | ||
| 364 | list := b.get("/proj/merge-requests", nil) | 364 | list := b.get("/proj/merge-requests", nil) | ||
| 365 | - | assert.Contains(t, list.body, "merged and closed") | 365 | + | assert.Contains(t, list.body, "Merged and closed") |
| 366 | - | assert.Regexp(t, `(?s)merged and closed.*Add search`, list.body) | 366 | + | assert.Regexp(t, `(?s)Merged and closed.*Add search`, list.body) |
| 367 | } | 367 | } | ||
| 368 | 368 | ||||
| 369 | // Squash and close, and the order of the list: open first, then the decided. | 369 | // Squash and close, and the order of the list: open first, then the decided. | ||
| @@ -418,7 +418,7 @@ func TestWebMergeRequestSquashCloseAndList(t *testing.T) { | |||||
| 418 | require.True(t, third >= 0 && squash >= 0 && other >= 0, list) | 418 | require.True(t, third >= 0 && squash >= 0 && other >= 0, list) | ||
| 419 | assert.Less(t, third, squash, "the open request comes first") | 419 | assert.Less(t, third, squash, "the open request comes first") | ||
| 420 | assert.Less(t, third, other) | 420 | assert.Less(t, third, other) | ||
| 421 | - | assert.Less(t, strings.Index(list, ">open<"), strings.Index(list, "merged and closed")) | 421 | + | assert.Less(t, strings.Index(list, ">Open<"), strings.Index(list, "Merged and closed")) |
| 422 | assert.Less(t, other, squash, "newest first among the decided") | 422 | assert.Less(t, other, squash, "newest first among the decided") | ||
| 423 | 423 | ||||
| 424 | // A compare page of a branch that is behind says so. | 424 | // A compare page of a branch that is behind says so. | ||
M cmd/ermite/web_session_test.go +2 -2
| @@ -116,7 +116,7 @@ func TestWebLoginEndToEnd(t *testing.T) { | |||||
| 116 | assert.NotContains(t, home.body, "secret") | 116 | assert.NotContains(t, home.body, "secret") | ||
| 117 | assert.Equal(t, "no-cache", home.hdr.Get("Cache-Control")) | 117 | assert.Equal(t, "no-cache", home.hdr.Get("Cache-Control")) | ||
| 118 | assert.Empty(t, home.hdr.Values("Vary")) | 118 | assert.Empty(t, home.hdr.Values("Vary")) | ||
| 119 | - | assert.NotContains(t, home.body, "sign out") | 119 | + | assert.NotContains(t, home.body, `action="/-/logout"`) |
| 120 | assert.Equal(t, 404, b.get("/secret", nil).status) | 120 | assert.Equal(t, 404, b.get("/secret", nil).status) | ||
| 121 | 121 | ||||
| 122 | // The CLI asks for a link and prints it; stderr says when it ends. | 122 | // The CLI asks for a link and prints it; stderr says when it ends. | ||
| @@ -198,7 +198,7 @@ func TestWebLoginEndToEnd(t *testing.T) { | |||||
| 198 | } | 198 | } | ||
| 199 | home = b.get("/", as(token)) | 199 | home = b.get("/", as(token)) | ||
| 200 | assert.Contains(t, home.body, "secret") | 200 | assert.Contains(t, home.body, "secret") | ||
| 201 | - | assert.Contains(t, home.body, `<form class="st-owner" method="post" action="/-/logout">`) | 201 | + | assert.Contains(t, home.body, `<form class="owner" method="post" action="/-/logout">`) |
| 202 | assert.Contains(t, b.get("/secret/blob/main/notes.txt", as(token)).body, "notes.txt") | 202 | assert.Contains(t, b.get("/secret/blob/main/notes.txt", as(token)).body, "notes.txt") | ||
| 203 | raw := b.get("/secret/raw/main/notes.txt", as(token)) | 203 | raw := b.get("/secret/raw/main/notes.txt", as(token)) | ||
| 204 | assert.Equal(t, 200, raw.status) | 204 | assert.Equal(t, 200, raw.status) | ||
M cmd/ermite/web_test.go +19 -2
| @@ -184,6 +184,7 @@ var ( | |||||
| 184 | reStyleAttr = regexp.MustCompile(`(?i)\sstyle\s*=`) | 184 | reStyleAttr = regexp.MustCompile(`(?i)\sstyle\s*=`) | ||
| 185 | reStyleTag = regexp.MustCompile(`(?i)<style\b`) | 185 | reStyleTag = regexp.MustCompile(`(?i)<style\b`) | ||
| 186 | reImport = regexp.MustCompile(`(?i)@import|url\(`) | 186 | reImport = regexp.MustCompile(`(?i)@import|url\(`) | ||
| 187 | + | reCSSURL = regexp.MustCompile(`url\(\s*([^)]*?)\s*\)`) | |||
| 187 | ) | 188 | ) | ||
| 188 | 189 | ||||
| 189 | // assertNoInline checks the page has no inline script, style element or | 190 | // assertNoInline checks the page has no inline script, style element or | ||
| @@ -231,14 +232,30 @@ func TestWebContentSecurity(t *testing.T) { | |||||
| 231 | assert.Equal(t, "public, max-age=31536000, immutable", ah.Get("Cache-Control"), m[1]) | 232 | assert.Equal(t, "public, max-age=31536000, immutable", ah.Get("Cache-Control"), m[1]) | ||
| 232 | assert.Equal(t, "nosniff", ah.Get("X-Content-Type-Options"), m[1]) | 233 | assert.Equal(t, "nosniff", ah.Get("X-Content-Type-Options"), m[1]) | ||
| 233 | assert.NotEmpty(t, ab) | 234 | assert.NotEmpty(t, ab) | ||
| 234 | - | assert.False(t, reImport.MatchString(ab) && strings.HasSuffix(m[1], ".css"), "%s imports or loads a URL", m[1]) | 235 | + | if strings.HasSuffix(m[1], ".css") { |
| 236 | + | // The sheet loads its fonts and icons from here, and nothing else. | |||
| 237 | + | assert.NotContains(t, ab, "@import", m[1]) | |||
| 238 | + | for _, u := range reCSSURL.FindAllStringSubmatch(ab, -1) { | |||
| 239 | + | assert.True(t, strings.HasPrefix(u[1], "/-/static/"), "%s loads %s", m[1], u[1]) | |||
| 240 | + | st, _, _ := h.get(u[1]) | |||
| 241 | + | assert.Equal(t, 200, st, u[1]) | |||
| 242 | + | } | |||
| 243 | + | } | |||
| 235 | } | 244 | } | ||
| 236 | } | 245 | } | ||
| 237 | 246 | ||||
| 238 | // The footer links to the source (AGPL §13). | 247 | // The footer links to the source (AGPL §13). | ||
| 239 | - | _, _, body := h.get("/") | 248 | + | _, hdr, body := h.get("/") |
| 240 | assert.Contains(t, body, `<a href="http://example.test/ermite"`) | 249 | assert.Contains(t, body, `<a href="http://example.test/ermite"`) | ||
| 241 | 250 | ||||
| 251 | + | // Pages name their speculation rules (prefetch on hover), a static file. | |||
| 252 | + | rules := strings.Trim(hdr.Get("Speculation-Rules"), `"`) | |||
| 253 | + | require.True(t, strings.HasPrefix(rules, "/-/static/rules/rules."), rules) | |||
| 254 | + | st, rh, rb := h.get(rules) | |||
| 255 | + | assert.Equal(t, 200, st) | |||
| 256 | + | assert.Equal(t, "application/speculationrules+json", rh.Get("Content-Type")) | |||
| 257 | + | assert.Contains(t, rb, `"prefetch"`) | |||
| 258 | + | ||||
| 242 | // Unknown, stale or traversing asset names are 404s. | 259 | // Unknown, stale or traversing asset names are 404s. | ||
| 243 | for _, p := range []string{"/-/static/css/ermite.000000000000.css", "/-/static/css/ermite.css", "/-/static/", "/-/static/../x", "/-/static/js/"} { | 260 | for _, p := range []string{"/-/static/css/ermite.000000000000.css", "/-/static/css/ermite.css", "/-/static/", "/-/static/../x", "/-/static/js/"} { | ||
| 244 | st, _, _ := h.get(p) | 261 | st, _, _ := h.get(p) | ||
M internal/adapters/web/ASSETS.md +5 -1
| @@ -10,7 +10,11 @@ content-hashed names. Nothing is fetched at runtime. | |||||
| 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | ||
| 11 | | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | | | 11 | | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | | | ||
| 12 | | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | | | 12 | | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | | | ||
| 13 | - | | `static/css/*.css` | native CSS of design D, served as one stylesheet in name order | `mockups/d/css/` (+ `70-footer.css`, the AGPL §13 source link) | | | 13 | + | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | |
| 14 | + | | `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` | | |||
| 15 | + | | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` | | |||
| 16 | + | | `static/icons/*.svg` | interface icons, used as CSS masks | written here | | | |||
| 17 | + | | `static/rules.json` | speculation rules: prefetch same-origin pages on hover (`Speculation-Rules` header) | written here | | | |||
| 14 | 18 | ||||
| 15 | The three htmx files are the exact files the approved mockup was built with. | 19 | The three htmx files are the exact files the approved mockup was built with. | ||
| 16 | To upgrade, replace the file, update the checksum here, and run the web tests | 20 | To upgrade, replace the file, update the checksum here, and run the web tests | ||
M internal/adapters/web/assets.go +41 -2
| @@ -23,6 +23,10 @@ const StaticPrefix = "/-/static/" | |||||
| 23 | // the content, so they can be cached for ever. | 23 | // the content, so they can be cached for ever. | ||
| 24 | type Assets struct { | 24 | type Assets struct { | ||
| 25 | CSS, HTMX, Live, SSE, App, Icon string | 25 | CSS, HTMX, Live, SSE, App, Icon string | ||
| 26 | + | // Font is the UI font, preloaded by every page. | |||
| 27 | + | Font string | |||
| 28 | + | // Rules is the speculation rules file named by the Speculation-Rules header. | |||
| 29 | + | Rules string | |||
| 26 | } | 30 | } | ||
| 27 | 31 | ||||
| 28 | // asset is one embedded file under its content-hashed name. | 32 | // asset is one embedded file under its content-hashed name. | ||
| @@ -49,8 +53,16 @@ var scripts = []struct { | |||||
| 49 | {"ermite.js", func(a *Assets, u string) { a.App = u }}, | 53 | {"ermite.js", func(a *Assets, u string) { a.App = u }}, | ||
| 50 | } | 54 | } | ||
| 51 | 55 | ||||
| 56 | + | // sheetAssets are the files the stylesheet references as url(<dir>/<file>): | |||
| 57 | + | // fonts and icons (pages hold no inline SVG; icons are CSS masks). | |||
| 58 | + | var sheetAssets = []struct{ glob, typ string }{ | |||
| 59 | + | {"static/fonts/*.woff2", "font/woff2"}, | |||
| 60 | + | {"static/icons/*.svg", "image/svg+xml"}, | |||
| 61 | + | } | |||
| 62 | + | ||||
| 52 | // loadAssets hashes the embedded files. The stylesheet is the concatenation | 63 | // loadAssets hashes the embedded files. The stylesheet is the concatenation | ||
| 53 | - | // of static/css/*.css in name order, as in the mockup. | 64 | + | // of static/css/*.css in name order; its url(<dir>/<file>) references become |
| 65 | + | // the hashed URLs of the files of sheetAssets. | |||
| 54 | func loadAssets() (*assetSet, error) { | 66 | func loadAssets() (*assetSet, error) { | ||
| 55 | s := &assetSet{byName: map[string]asset{}} | 67 | s := &assetSet{byName: map[string]asset{}} | ||
| 56 | put := func(dir, file string, body []byte, typ string) string { | 68 | put := func(dir, file string, body []byte, typ string) string { | ||
| @@ -61,6 +73,26 @@ func loadAssets() (*assetSet, error) { | |||||
| 61 | return StaticPrefix + name | 73 | return StaticPrefix + name | ||
| 62 | } | 74 | } | ||
| 63 | 75 | ||||
| 76 | + | var refs []string | |||
| 77 | + | for _, sa := range sheetAssets { | |||
| 78 | + | files, err := fs.Glob(staticFS, sa.glob) | |||
| 79 | + | if err != nil { | |||
| 80 | + | return nil, err | |||
| 81 | + | } | |||
| 82 | + | for _, f := range files { | |||
| 83 | + | b, err := staticFS.ReadFile(f) | |||
| 84 | + | if err != nil { | |||
| 85 | + | return nil, err | |||
| 86 | + | } | |||
| 87 | + | dir := path.Base(path.Dir(f)) | |||
| 88 | + | u := put(dir, path.Base(f), b, sa.typ) | |||
| 89 | + | refs = append(refs, "url("+dir+"/"+path.Base(f)+")", "url("+u+")") | |||
| 90 | + | if f == "static/fonts/atkinson-next.woff2" { | |||
| 91 | + | s.urls.Font = u | |||
| 92 | + | } | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | ||||
| 64 | files, err := fs.Glob(staticFS, "static/css/*.css") | 96 | files, err := fs.Glob(staticFS, "static/css/*.css") | ||
| 65 | if err != nil { | 97 | if err != nil { | ||
| 66 | return nil, err | 98 | return nil, err | ||
| @@ -75,7 +107,14 @@ func loadAssets() (*assetSet, error) { | |||||
| 75 | css.WriteString("/* " + path.Base(f) + " */\n") | 107 | css.WriteString("/* " + path.Base(f) + " */\n") | ||
| 76 | css.Write(b) | 108 | css.Write(b) | ||
| 77 | } | 109 | } | ||
| 78 | - | s.urls.CSS = put("css", "ermite.css", []byte(css.String()), "text/css; charset=utf-8") | 110 | + | sheet := strings.NewReplacer(refs...).Replace(css.String()) |
| 111 | + | s.urls.CSS = put("css", "ermite.css", []byte(sheet), "text/css; charset=utf-8") | |||
| 112 | + | ||||
| 113 | + | rules, err := staticFS.ReadFile("static/rules.json") | |||
| 114 | + | if err != nil { | |||
| 115 | + | return nil, err | |||
| 116 | + | } | |||
| 117 | + | s.urls.Rules = put("rules", "rules.json", rules, "application/speculationrules+json") | |||
| 79 | 118 | ||||
| 80 | for _, sc := range scripts { | 119 | for _, sc := range scripts { | ||
| 81 | b, err := staticFS.ReadFile("static/js/" + sc.file) | 120 | b, err := staticFS.ReadFile("static/js/" + sc.file) | ||
M internal/adapters/web/blob.go +1 -5
| @@ -188,11 +188,7 @@ func (s *Server) blob(ctx context.Context, r *http.Request) (templ.Component, in | |||||
| 188 | pinRef(&l, t) | 188 | pinRef(&l, t) | ||
| 189 | l.Crumbs = pathCrumbs(d.name(), "blob", t) | 189 | l.Crumbs = pathCrumbs(d.name(), "blob", t) | ||
| 190 | l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance | 190 | l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance | ||
| 191 | - | l.Status = Status{ | 191 | + | l.HasRail = true |
| 192 | - | Mode: "blob", Context: p.Name, | |||
| 193 | - | Hints: "click a line number · y permalink · ? help", Blob: p.HasCode, | |||
| 194 | - | } | |||
| 195 | - | l.HasRail, l.Read, l.RailSum = true, p.Rendered != nil, "file · last change · selection" | |||
| 196 | return Blob(l, p), http.StatusOK, nil | 192 | return Blob(l, p), http.StatusOK, nil | ||
| 197 | } | 193 | } | ||
| 198 | 194 | ||||
M internal/adapters/web/blob.templ +6 -6
| @@ -12,7 +12,7 @@ templ Blob(l Layout, p BlobPage) { | |||||
| 12 | @Shell(l, blobRail(p)) { | 12 | @Shell(l, blobRail(p)) { | ||
| 13 | <h1 class="file-h"> | 13 | <h1 class="file-h"> | ||
| 14 | <span>{ p.Name }</span> | 14 | <span>{ p.Name }</span> | ||
| 15 | - | <span class="acts"><a href={ templ.SafeURL(p.RawHref) }>raw</a> <a href={ templ.SafeURL(p.HistHref) }>history</a> <a href={ templ.SafeURL(p.DirHref) }>dir</a></span> | 15 | + | <span class="acts"><a class="btn" href={ templ.SafeURL(p.RawHref) }>Raw</a><a class="btn" href={ templ.SafeURL(p.HistHref) }>History</a><a class="btn" href={ templ.SafeURL(p.DirHref) }>Folder</a></span> |
| 16 | </h1> | 16 | </h1> | ||
| 17 | if p.Notice != "" { | 17 | if p.Notice != "" { | ||
| 18 | <p class="notice" role="note">{ p.Notice }</p> | 18 | <p class="notice" role="note">{ p.Notice }</p> | ||
| @@ -39,7 +39,7 @@ templ Blob(l Layout, p BlobPage) { | |||||
| 39 | // CodeView is the highlighted lines with a link on each number. A click on a | 39 | // CodeView is the highlighted lines with a link on each number. A click on a | ||
| 40 | // number selects the line, shift-click extends the selection to a range; the | 40 | // number selects the line, shift-click extends the selection to a range; the | ||
| 41 | // URL hash (#L10 or #L10-L24) is the state, so a page loaded with a hash is | 41 | // URL hash (#L10 or #L10-L24) is the state, so a page loaded with a hash is | ||
| 42 | - | // painted the same way. body[data-sel] feeds the status bar and the rail. | 42 | + | // painted the same way. body[data-sel] feeds the hint badge and the rail. |
| 43 | templ CodeView(h template.HTML) { | 43 | templ CodeView(h template.HTML) { | ||
| 44 | <pre | 44 | <pre | ||
| 45 | class="chroma code" | 45 | class="chroma code" | ||
| @@ -59,12 +59,12 @@ templ CodeView(h template.HTML) { | |||||
| 59 | 59 | ||||
| 60 | templ blobRail(p BlobPage) { | 60 | templ blobRail(p BlobPage) { | ||
| 61 | <section aria-labelledby="r-file"> | 61 | <section aria-labelledby="r-file"> | ||
| 62 | - | <h2 id="r-file">file</h2> | 62 | + | <h2 id="r-file">File</h2> |
| 63 | @Facts(p.Facts) | 63 | @Facts(p.Facts) | ||
| 64 | </section> | 64 | </section> | ||
| 65 | if p.Last != nil { | 65 | if p.Last != nil { | ||
| 66 | <section aria-labelledby="r-last"> | 66 | <section aria-labelledby="r-last"> | ||
| 67 | - | <h2 id="r-last">last change <a href={ templ.SafeURL(p.HistHref) }>history</a></h2> | 67 | + | <h2 id="r-last">Last change <a href={ templ.SafeURL(p.HistHref) }>History</a></h2> |
| 68 | <p><a href={ templ.SafeURL(p.Last.CHref) }>{ p.Last.Short }</a> { p.Last.Subject }</p> | 68 | <p><a href={ templ.SafeURL(p.Last.CHref) }>{ p.Last.Short }</a> { p.Last.Subject }</p> | ||
| 69 | <p class="dim"> | 69 | <p class="dim"> | ||
| 70 | { p.Last.Author } · | 70 | { p.Last.Author } · | ||
| @@ -76,13 +76,13 @@ templ blobRail(p BlobPage) { | |||||
| 76 | } | 76 | } | ||
| 77 | if len(p.Outline) > 0 { | 77 | if len(p.Outline) > 0 { | ||
| 78 | <section aria-labelledby="r-outline"> | 78 | <section aria-labelledby="r-outline"> | ||
| 79 | - | <h2 id="r-outline">outline</h2> | 79 | + | <h2 id="r-outline">On this page</h2> |
| 80 | @Outline(p.Outline) | 80 | @Outline(p.Outline) | ||
| 81 | </section> | 81 | </section> | ||
| 82 | } | 82 | } | ||
| 83 | if p.HasCode { | 83 | if p.HasCode { | ||
| 84 | <section aria-labelledby="r-sel"> | 84 | <section aria-labelledby="r-sel"> | ||
| 85 | - | <h2 id="r-sel">selection</h2> | 85 | + | <h2 id="r-sel">Selection</h2> |
| 86 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | 86 | <p :text="document.body.dataset.sel || 'none · click a line number, shift-click extends'">none · click a line number, shift-click extends</p> | ||
| 87 | <p class="dim">y copies a permalink pinned to the commit sha</p> | 87 | <p class="dim">y copies a permalink pinned to the commit sha</p> | ||
| 88 | </section> | 88 | </section> | ||
M internal/adapters/web/blob_templ.go +13 -13 generated file: collapsed
M internal/adapters/web/commit.go +1 -10
| @@ -157,19 +157,10 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 157 | {Label: "commit", Href: history}, | 157 | {Label: "commit", Href: history}, | ||
| 158 | {Label: p.Short, Current: true}, | 158 | {Label: p.Short, Current: true}, | ||
| 159 | } | 159 | } | ||
| 160 | - | l.Status = Status{ | 160 | + | l.HasRail = true |
| 161 | - | Mode: "commit", Context: p.Short + " · " + p.totals(), | |||
| 162 | - | Hints: "d split · u unified · x collapse all · ? help", | |||
| 163 | - | } | |||
| 164 | - | l.HasRail, l.RailSum = true, plural(len(p.Files), "changed file", "changed files") | |||
| 165 | return Commit(l, p), http.StatusOK, nil | 161 | return Commit(l, p), http.StatusOK, nil | ||
| 166 | } | 162 | } | ||
| 167 | 163 | ||||
| 168 | - | // totals is "3 files · +12 -4". | |||
| 169 | - | func (p CommitPage) totals() string { | |||
| 170 | - | return plural(p.NFiles, "file", "files") + " · +" + itoa(p.Adds) + " -" + itoa(p.Dels) | |||
| 171 | - | } | |||
| 172 | - | ||||
| 173 | // person converts a signature. | 164 | // person converts a signature. | ||
| 174 | func person(s *pb.Signature, now time.Time) Person { | 165 | func person(s *pb.Signature, now time.Time) Person { | ||
| 175 | p := Person{Name: visibleText(s.GetName())} | 166 | p := Person{Name: visibleText(s.GetName())} | ||
M internal/adapters/web/commits.go +1 -12
| @@ -94,18 +94,7 @@ func (s *Server) commits(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 94 | } else { | 94 | } else { | ||
| 95 | l.Title = "history of " + visibleText(sel.path) + " · " + d.name() + " · " + s.instance | 95 | l.Title = "history of " + visibleText(sel.path) + " · " + d.name() + " · " + s.instance | ||
| 96 | } | 96 | } | ||
| 97 | - | what := plural(len(cs), "commit", "commits") | 97 | + | l.HasRail = true |
| 98 | - | if p.Older != "" { | |||
| 99 | - | what += " · more" | |||
| 100 | - | } | |||
| 101 | - | if p.Path != "" { | |||
| 102 | - | what = "history of " + p.Path + " · " + what | |||
| 103 | - | } | |||
| 104 | - | l.Status = Status{ | |||
| 105 | - | Mode: "commits", Context: sel.name + " · " + what, | |||
| 106 | - | Hints: "j/k move · enter open · / filter · ? help", Cursor: true, Count: len(cs), | |||
| 107 | - | } | |||
| 108 | - | l.HasRail, l.RailSum = true, "history · refs on this page" | |||
| 109 | return Commits(l, p), http.StatusOK, nil | 98 | return Commits(l, p), http.StatusOK, nil | ||
| 110 | } | 99 | } | ||
| 111 | 100 | ||||
M internal/adapters/web/commits.templ +8 -8
| @@ -7,12 +7,12 @@ package web | |||||
| 7 | templ Commits(l Layout, p CommitsPage) { | 7 | templ Commits(l Layout, p CommitsPage) { | ||
| 8 | @Shell(l, commitsRail(p)) { | 8 | @Shell(l, commitsRail(p)) { | ||
| 9 | <div class="filter"> | 9 | <div class="filter"> | ||
| 10 | - | <label for="commit-filter">filter></label> | 10 | + | <label for="commit-filter" class="sr">filter</label> |
| 11 | <input | 11 | <input | ||
| 12 | id="commit-filter" | 12 | id="commit-filter" | ||
| 13 | type="search" | 13 | type="search" | ||
| 14 | data-filter | 14 | data-filter | ||
| 15 | - | placeholder="subject, sha or author ( / )" | 15 | + | placeholder="Filter by subject, sha or author…" |
| 16 | autocomplete="off" | 16 | autocomplete="off" | ||
| 17 | hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | 17 | hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | ||
| 18 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | 18 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | ||
| @@ -39,14 +39,14 @@ templ Commits(l Layout, p CommitsPage) { | |||||
| 39 | if p.Older != "" || p.Newest != "" { | 39 | if p.Older != "" || p.Newest != "" { | ||
| 40 | <nav class="pager" aria-label="Pages"> | 40 | <nav class="pager" aria-label="Pages"> | ||
| 41 | if p.Newest != "" { | 41 | if p.Newest != "" { | ||
| 42 | - | <a href={ templ.SafeURL(p.Newest) }>← newest</a> | 42 | + | <a class="btn" href={ templ.SafeURL(p.Newest) }>← Newest</a> |
| 43 | } else { | 43 | } else { | ||
| 44 | - | <span class="mute">← newest</span> | 44 | + | <span class="btn" aria-disabled="true">← Newest</span> |
| 45 | } | 45 | } | ||
| 46 | if p.Older != "" { | 46 | if p.Older != "" { | ||
| 47 | - | <a href={ templ.SafeURL(p.Older) } rel="next">older →</a> | 47 | + | <a class="btn" href={ templ.SafeURL(p.Older) } rel="next">Older →</a> |
| 48 | } else { | 48 | } else { | ||
| 49 | - | <span class="mute">older →</span> | 49 | + | <span class="btn" aria-disabled="true">Older →</span> |
| 50 | } | 50 | } | ||
| 51 | </nav> | 51 | </nav> | ||
| 52 | } | 52 | } | ||
| @@ -55,7 +55,7 @@ templ Commits(l Layout, p CommitsPage) { | |||||
| 55 | 55 | ||||
| 56 | templ commitsRail(p CommitsPage) { | 56 | templ commitsRail(p CommitsPage) { | ||
| 57 | <section aria-labelledby="r-hist"> | 57 | <section aria-labelledby="r-hist"> | ||
| 58 | - | <h2 id="r-hist">history</h2> | 58 | + | <h2 id="r-hist">History</h2> |
| 59 | <dl class="kv"> | 59 | <dl class="kv"> | ||
| 60 | <dt>ref</dt> | 60 | <dt>ref</dt> | ||
| 61 | <dd>{ p.Ref }</dd> | 61 | <dd>{ p.Ref }</dd> | ||
| @@ -72,7 +72,7 @@ templ commitsRail(p CommitsPage) { | |||||
| 72 | </dl> | 72 | </dl> | ||
| 73 | </section> | 73 | </section> | ||
| 74 | <section aria-labelledby="r-refs"> | 74 | <section aria-labelledby="r-refs"> | ||
| 75 | - | <h2 id="r-refs">refs on this page</h2> | 75 | + | <h2 id="r-refs">Refs on this page</h2> |
| 76 | if len(p.Refs) > 0 { | 76 | if len(p.Refs) > 0 { | ||
| 77 | <nav class="outline" aria-label="Refs on this page"> | 77 | <nav class="outline" aria-label="Refs on this page"> | ||
| 78 | for _, r := range p.Refs { | 78 | for _, r := range p.Refs { | ||
M internal/adapters/web/commits_templ.go +11 -11 generated file: collapsed
M internal/adapters/web/compare.go +1 -5
| @@ -132,11 +132,7 @@ func (s *Server) compare(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 132 | Crumb{Label: "compare", Current: false}, | 132 | Crumb{Label: "compare", Current: false}, | ||
| 133 | Crumb{Label: p.Source, Current: true}, | 133 | Crumb{Label: p.Source, Current: true}, | ||
| 134 | ) | 134 | ) | ||
| 135 | - | l.Status = Status{ | 135 | + | l.HasRail = true |
| 136 | - | Mode: "compare", Context: p.Source + " → " + p.Target + " · " + p.AheadText, | |||
| 137 | - | Hints: "d split · u unified · x collapse all · ? help", | |||
| 138 | - | } | |||
| 139 | - | l.HasRail, l.RailSum = true, "commits · files" | |||
| 140 | return Compare(l, p), http.StatusOK, nil | 136 | return Compare(l, p), http.StatusOK, nil | ||
| 141 | } | 137 | } | ||
| 142 | 138 | ||||
M internal/adapters/web/components.templ +19 -8
| @@ -21,11 +21,11 @@ templ CloneBlock(c Clone) { | |||||
| 21 | <span class="mute" aria-hidden="true">$ </span><code>{ line }</code> | 21 | <span class="mute" aria-hidden="true">$ </span><code>{ line }</code> | ||
| 22 | <button | 22 | <button | ||
| 23 | type="button" | 23 | type="button" | ||
| 24 | - | class="copy" | 24 | + | class="btn copy" |
| 25 | - | data-label="[copy]" | 25 | + | data-label="Copy" |
| 26 | aria-label={ "copy: " + line } | 26 | aria-label={ "copy: " + line } | ||
| 27 | - | hx-on:click="navigator.clipboard?.writeText(closest('.clone-line').querySelector('code').textContent); this.textContent = '[copied]'; await timeout('1500ms'); this.textContent = this.dataset.label" | 27 | + | hx-on:click="navigator.clipboard?.writeText(closest('.clone-line').querySelector('code').textContent); this.textContent = 'Copied'; await timeout('1500ms'); this.textContent = this.dataset.label" |
| 28 | - | >[copy]</button> | 28 | + | >Copy</button> |
| 29 | </div> | 29 | </div> | ||
| 30 | } | 30 | } | ||
| 31 | </div> | 31 | </div> | ||
| @@ -38,7 +38,7 @@ templ HomeRowView(r HomeRow) { | |||||
| 38 | <td class="c-name"> | 38 | <td class="c-name"> | ||
| 39 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | 39 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | ||
| 40 | if r.Fresh { | 40 | if r.Fresh { | ||
| 41 | - | <span class="tagged">● new push</span> | 41 | + | <span class="pill new">new push</span> |
| 42 | } | 42 | } | ||
| 43 | </td> | 43 | </td> | ||
| 44 | <td class="c-desc dim">{ r.Desc }</td> | 44 | <td class="c-desc dim">{ r.Desc }</td> | ||
| @@ -62,6 +62,17 @@ templ Outline(headings []Heading) { | |||||
| 62 | </nav> | 62 | </nav> | ||
| 63 | } | 63 | } | ||
| 64 | 64 | ||||
| 65 | + | // ReadmeCard is a README in a card whose header names the file. | |||
| 66 | + | templ ReadmeCard(r Readme) { | |||
| 67 | + | <section class="card readme" aria-labelledby="readme-h"> | |||
| 68 | + | <h2 class="card-h" id="readme-h"> | |||
| 69 | + | <span class="ico i-file" aria-hidden="true"></span> | |||
| 70 | + | { r.Name } | |||
| 71 | + | </h2> | |||
| 72 | + | @ReadmeView(r) | |||
| 73 | + | </section> | |||
| 74 | + | } | |||
| 75 | + | ||||
| 65 | // ReadmeView is a rendered README. Its HTML comes from the Markdown | 76 | // ReadmeView is a rendered README. Its HTML comes from the Markdown | ||
| 66 | // renderer, which drops raw HTML and filters URLs. | 77 | // renderer, which drops raw HTML and filters URLs. | ||
| 67 | templ ReadmeView(r Readme) { | 78 | templ ReadmeView(r Readme) { | ||
| @@ -91,13 +102,13 @@ templ CommitRows(rows []CommitRow) { | |||||
| 91 | <td class="c-subject"> | 102 | <td class="c-subject"> | ||
| 92 | <a href={ templ.SafeURL(c.CHref) }>{ c.Subject }</a> | 103 | <a href={ templ.SafeURL(c.CHref) }>{ c.Subject }</a> | ||
| 93 | if c.Merge { | 104 | if c.Merge { | ||
| 94 | - | <span class="tagged">(merge)</span> | 105 | + | <span class="pill">merge</span> |
| 95 | } | 106 | } | ||
| 96 | for _, b := range c.Branches { | 107 | for _, b := range c.Branches { | ||
| 97 | - | <span class="tagged">[{ b }]</span> | 108 | + | <span class="pill">{ b }</span> |
| 98 | } | 109 | } | ||
| 99 | for _, t := range c.Tags { | 110 | for _, t := range c.Tags { | ||
| 100 | - | <span class="tagged">[tag: { t }]</span> | 111 | + | <span class="pill tag"><span class="sr">tag </span>{ t }</span> |
| 101 | } | 112 | } | ||
| 102 | </td> | 113 | </td> | ||
| 103 | <td class="c-who dim">{ c.Author }</td> | 114 | <td class="c-who dim">{ c.Author }</td> | ||
M internal/adapters/web/components_templ.go +132 -81 generated file: collapsed
M internal/adapters/web/diff.templ +7 -7
| @@ -44,7 +44,7 @@ templ Commit(l Layout, p CommitPage) { | |||||
| 44 | <dt>refs</dt> | 44 | <dt>refs</dt> | ||
| 45 | <dd> | 45 | <dd> | ||
| 46 | for _, r := range p.Refs { | 46 | for _, r := range p.Refs { | ||
| 47 | - | [{ r }]{ " " } | 47 | + | <span class="pill">{ r }</span> |
| 48 | } | 48 | } | ||
| 49 | </dd> | 49 | </dd> | ||
| 50 | } | 50 | } | ||
| @@ -53,9 +53,9 @@ templ Commit(l Layout, p CommitPage) { | |||||
| 53 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | 53 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | ||
| 54 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | 54 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | ||
| 55 | <span class="grow"></span> | 55 | <span class="grow"></span> | ||
| 56 | - | @viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref) | 56 | + | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) |
| 57 | - | @viewLink(p.DiffView, "split", "[d] split", "d", p.SplitHref) | 57 | + | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) |
| 58 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 58 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd></button> |
| 59 | </div> | 59 | </div> | ||
| 60 | if p.Invisible { | 60 | if p.Invisible { | ||
| 61 | <p class="notice"> | 61 | <p class="notice"> | ||
| @@ -99,7 +99,7 @@ templ Commit(l Layout, p CommitPage) { | |||||
| 99 | templ viewLink(p DiffView, view, label, key, href string) { | 99 | templ viewLink(p DiffView, view, label, key, href string) { | ||
| 100 | <a | 100 | <a | ||
| 101 | href={ templ.SafeURL(href) } | 101 | href={ templ.SafeURL(href) } | ||
| 102 | - | class="vbtn" | 102 | + | class="btn vbtn" |
| 103 | data-view={ view } | 103 | data-view={ view } | ||
| 104 | aria-keyshortcuts={ key } | 104 | aria-keyshortcuts={ key } | ||
| 105 | if p.View == view { | 105 | if p.View == view { | ||
| @@ -109,7 +109,7 @@ templ viewLink(p DiffView, view, label, key, href string) { | |||||
| 109 | hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" | 109 | hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" | ||
| 110 | :aria-current={ "q('#diffs').attr['data-diff'] === '" + view + "' ? 'true' : false" } | 110 | :aria-current={ "q('#diffs').attr['data-diff'] === '" + view + "' ? 'true' : false" } | ||
| 111 | } | 111 | } | ||
| 112 | - | >{ label }</a> | 112 | + | >{ label }<kbd>{ key }</kbd></a> |
| 113 | } | 113 | } | ||
| 114 | 114 | ||||
| 115 | templ personView(pr Person) { | 115 | templ personView(pr Person) { | ||
| @@ -121,7 +121,7 @@ templ personView(pr Person) { | |||||
| 121 | 121 | ||||
| 122 | templ commitRail(p CommitPage) { | 122 | templ commitRail(p CommitPage) { | ||
| 123 | <section aria-labelledby="r-files"> | 123 | <section aria-labelledby="r-files"> | ||
| 124 | - | <h2 id="r-files">files <span>{ itoa(len(p.Files)) }</span></h2> | 124 | + | <h2 id="r-files">Files <span class="count">{ itoa(len(p.Files)) }</span></h2> |
| 125 | @FileList(p.Files) | 125 | @FileList(p.Files) | ||
| 126 | </section> | 126 | </section> | ||
| 127 | } | 127 | } | ||
M internal/adapters/web/diff_templ.go +139 -135 generated file: collapsed
M internal/adapters/web/events.go +1 -1
| @@ -19,7 +19,7 @@ import ( | |||||
| 19 | ) | 19 | ) | ||
| 20 | 20 | ||||
| 21 | // Live updates. A page that can change under the eyes of its reader opens | 21 | // Live updates. A page that can change under the eyes of its reader opens | ||
| 22 | - | // GET /-/events (the Live component of the status bar). The handler follows | 22 | + | // GET /-/events (the Live component of the hint badge). The handler follows |
| 23 | // EventService.Watch as the visitor of the request and turns each event into | 23 | // EventService.Watch as the visitor of the request and turns each event into | ||
| 24 | // the HTML fragments the page shows, sent as Server-Sent Events: | 24 | // the HTML fragments the page shows, sent as Server-Sent Events: | ||
| 25 | // | 25 | // | ||
M internal/adapters/web/format.go +0 -8
| @@ -10,14 +10,6 @@ import ( | |||||
| 10 | 10 | ||||
| 11 | func itoa(n int) string { return strconv.Itoa(n) } | 11 | func itoa(n int) string { return strconv.Itoa(n) } | ||
| 12 | 12 | ||||
| 13 | - | // cursorText is the initial text of the cursor position in the status bar. | |||
| 14 | - | func cursorText(n int) string { | |||
| 15 | - | if n == 0 { | |||
| 16 | - | return "0/0" | |||
| 17 | - | } | |||
| 18 | - | return "1/" + itoa(n) | |||
| 19 | - | } | |||
| 20 | - | ||||
| 21 | // ago renders a short relative date: now, 5m ago, 3h ago, 12d ago, 5mo ago, | 13 | // ago renders a short relative date: now, 5m ago, 3h ago, 12d ago, 5mo ago, | ||
| 22 | // 2y ago. | 14 | // 2y ago. | ||
| 23 | func ago(now, t time.Time) string { | 15 | func ago(now, t time.Time) string { | ||
M internal/adapters/web/fragments.templ +8 -11
| @@ -76,25 +76,22 @@ templ MRNotice(text, href string) { | |||||
| 76 | </div> | 76 | </div> | ||
| 77 | } | 77 | } | ||
| 78 | 78 | ||||
| 79 | - | // Live is the connection state of the event stream, in the status bar. | 79 | + | // Live is the connection to the event stream, in the hint badge. It shows |
| 80 | - | // Without a stream it reads "offline" and does nothing. The events of the | 80 | + | // only once a connection was lost: a page that never connected, or that is |
| 81 | - | // stream are fragments that ermiteLive (ermite.js) swaps in the page. | 81 | + | // live, says nothing. The events of the stream are fragments that ermiteLive |
| 82 | + | // (ermite.js) swaps in the page. A page without a stream renders nothing. | |||
| 82 | templ Live(l Layout) { | 83 | templ Live(l Layout) { | ||
| 83 | if l.Live && l.LiveURL != "" { | 84 | if l.Live && l.LiveURL != "" { | ||
| 84 | <span | 85 | <span | ||
| 85 | - | class="st-live" | 86 | + | class="hint-live" |
| 86 | - | data-state="offline" | 87 | + | data-state="connecting" |
| 87 | - | role="status" | |||
| 88 | hx-sse:connect={ l.LiveURL } | 88 | hx-sse:connect={ l.LiveURL } | ||
| 89 | hx-swap="none" | 89 | hx-swap="none" | ||
| 90 | - | :text="data.state" | 90 | + | :text="data.state === 'offline' ? 'live updates paused' : ''" |
| 91 | - | :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" | |||
| 92 | hx-on="htmx:sse:after:connection -> data.state = 'live'; | 91 | hx-on="htmx:sse:after:connection -> data.state = 'live'; | ||
| 93 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | 92 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | ||
| 94 | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | 93 | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | ||
| 95 | gap -> ermiteLive.reload()" | 94 | gap -> ermiteLive.reload()" | ||
| 96 | - | >offline</span> | 95 | + | ></span> |
| 97 | - | } else { | |||
| 98 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |||
| 99 | } | 96 | } | ||
| 100 | } | 97 | } | ||
M internal/adapters/web/fragments_templ.go +6 -10 generated file: collapsed
M internal/adapters/web/home.go +0 -11
| @@ -109,17 +109,6 @@ func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, in | |||||
| 109 | l := s.base() | 109 | l := s.base() | ||
| 110 | l.Title = s.instance | 110 | l.Title = s.instance | ||
| 111 | l.Live, l.LiveURL = true, liveURL("home", "") | 111 | l.Live, l.LiveURL = true, liveURL("home", "") | ||
| 112 | - | l.Sections = []Section{{Key: "h", Post: "ome", Href: "/", Current: true}} | |||
| 113 | - | dir := "▲" | |||
| 114 | - | if desc { | |||
| 115 | - | dir = "▼" | |||
| 116 | - | } | |||
| 117 | - | l.Status = Status{ | |||
| 118 | - | Mode: "home", | |||
| 119 | - | Context: plural(len(repos), "repo", "repos") + " · sort " + key + " " + dir, | |||
| 120 | - | Hints: "j/k move · enter open · / filter · ? help", | |||
| 121 | - | Cursor: true, Count: len(repos), | |||
| 122 | - | } | |||
| 123 | l.Cmd = append([]CmdItem{{Kind: "go", Label: "home", Href: "/"}}, repoCmds(repos, "")...) | 112 | l.Cmd = append([]CmdItem{{Kind: "go", Label: "home", Href: "/"}}, repoCmds(repos, "")...) | ||
| 124 | return Home(l, p), http.StatusOK, nil | 113 | return Home(l, p), http.StatusOK, nil | ||
| 125 | } | 114 | } | ||
M internal/adapters/web/keys.templ +23 -19
| @@ -9,40 +9,44 @@ templ Keys() { | |||||
| 9 | <div | 9 | <div | ||
| 10 | id="keys" | 10 | id="keys" | ||
| 11 | hidden | 11 | hidden | ||
| 12 | - | hx-on="k-j from:body -> ((d) => { | 12 | + | hx-on="k-j from:body -> (async (d) => { |
| 13 | if (q('dialog[open]').count) return; | 13 | if (q('dialog[open]').count) return; | ||
| 14 | let rows = q('[data-rows] > :not([hidden])').arr(); | 14 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 15 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 15 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 16 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 16 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 17 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 17 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 18 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 18 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 19 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 20 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 19 | })(1); | 21 | })(1); | ||
| 20 | - | k-k from:body -> ((d) => { | 22 | + | k-k from:body -> (async (d) => { |
| 21 | if (q('dialog[open]').count) return; | 23 | if (q('dialog[open]').count) return; | ||
| 22 | let rows = q('[data-rows] > :not([hidden])').arr(); | 24 | let rows = q('[data-rows] > :not([hidden])').arr(); | ||
| 23 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | 25 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | ||
| 24 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | 26 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | ||
| 25 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | 27 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; |
| 26 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | 28 | + | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); |
| 29 | + | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |||
| 30 | + | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |||
| 27 | })(-1); | 31 | })(-1); | ||
| 28 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | 32 | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | ||
| 29 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | 33 | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | ||
| 30 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | 34 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | ||
| 31 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | 35 | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | ||
| 32 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show(); | 36 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); |
| 33 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus()); | 37 | + | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); |
| 34 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus(); | 38 | + | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); |
| 35 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | 39 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | ||
| 36 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | 40 | + | k-gh from:body -> document.querySelector('.brand')?.click(); |
| 37 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 41 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 38 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 42 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 39 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 43 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 40 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | 44 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); |
| 41 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | 45 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); |
| 42 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | 46 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); |
| 43 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | 47 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); |
| 44 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 48 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 45 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | 49 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); |
| 46 | k-next from:body -> { | 50 | k-next from:body -> { | ||
| 47 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | 51 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | ||
| 48 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | 52 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | ||
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/mr.go +4 -13
| @@ -377,12 +377,7 @@ func (s *Server) mrList(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 377 | p.Live = true | 377 | p.Live = true | ||
| 378 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mrs") | 378 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mrs") | ||
| 379 | } | 379 | } | ||
| 380 | - | rows := len(p.Open) + len(p.Done) | 380 | + | l.HasRail = true |
| 381 | - | l.Status = Status{ | |||
| 382 | - | Mode: "merge requests", Context: itoa(len(p.Open)) + " open · " + itoa(len(p.Done)) + " decided", | |||
| 383 | - | Hints: "j/k move · enter open · / filter · ? help", Cursor: true, Count: rows, | |||
| 384 | - | } | |||
| 385 | - | l.HasRail, l.RailSum = true, "states · open one" | |||
| 386 | return MRList(l, p), http.StatusOK, nil | 381 | return MRList(l, p), http.StatusOK, nil | ||
| 387 | } | 382 | } | ||
| 388 | 383 | ||||
| @@ -557,11 +552,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||||
| 557 | } | 552 | } | ||
| 558 | p.Live = true | 553 | p.Live = true | ||
| 559 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.GetSourceBranch())) | 554 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.GetSourceBranch())) | ||
| 560 | - | l.Status = Status{ | 555 | + | l.HasRail = true |
| 561 | - | Mode: "merge request", Context: p.Number + " · " + p.Source + " → " + p.Target + " · " + p.State, | |||
| 562 | - | Hints: "n next note · d split · u unified · x collapse all · ? help", | |||
| 563 | - | } | |||
| 564 | - | l.HasRail, l.RailSum = true, "merge · checks · commits · files" | |||
| 565 | return MergeRequest(l, p), http.StatusOK, nil | 556 | return MergeRequest(l, p), http.StatusOK, nil | ||
| 566 | } | 557 | } | ||
| 567 | 558 | ||||
| @@ -714,9 +705,9 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []*pb.C | |||||
| 714 | } | 705 | } | ||
| 715 | } | 706 | } | ||
| 716 | if canWrite { | 707 | if canWrite { | ||
| 717 | - | v.Resolve = &ResolveForm{Action: resolveAction(c.GetId()), Value: !c.GetResolved(), Label: "[resolve]"} | 708 | + | v.Resolve = &ResolveForm{Action: resolveAction(c.GetId()), Value: !c.GetResolved(), Label: "Resolve"} |
| 718 | if c.GetResolved() { | 709 | if c.GetResolved() { | ||
| 719 | - | v.Resolve.Label = "[unresolve]" | 710 | + | v.Resolve.Label = "Unresolve" |
| 720 | } | 711 | } | ||
| 721 | } | 712 | } | ||
| 722 | switch { | 713 | switch { | ||
M internal/adapters/web/mr.templ +32 -32
| @@ -12,19 +12,19 @@ templ MRList(l Layout, p MRListPage) { | |||||
| 12 | <div id="mr-notice"></div> | 12 | <div id="mr-notice"></div> | ||
| 13 | } | 13 | } | ||
| 14 | <div class="filter"> | 14 | <div class="filter"> | ||
| 15 | - | <label for="mr-filter">filter></label> | 15 | + | <label for="mr-filter" class="sr">filter</label> |
| 16 | <input | 16 | <input | ||
| 17 | id="mr-filter" | 17 | id="mr-filter" | ||
| 18 | type="search" | 18 | type="search" | ||
| 19 | data-filter | 19 | data-filter | ||
| 20 | - | placeholder="title, branch, author or state ( / )" | 20 | + | placeholder="Filter by title, branch, author or state…" |
| 21 | autocomplete="off" | 21 | autocomplete="off" | ||
| 22 | hx-on:input="let t = this.value.toLowerCase(); q('.mrs tbody tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | 22 | hx-on:input="let t = this.value.toLowerCase(); q('.mrs tbody tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | ||
| 23 | let first = q('.mrs tbody tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.mrs tbody tr')" | 23 | let first = q('.mrs tbody tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.mrs tbody tr')" | ||
| 24 | /> | 24 | /> | ||
| 25 | </div> | 25 | </div> | ||
| 26 | if p.Newest == "" { | 26 | if p.Newest == "" { | ||
| 27 | - | <h2 class="rule-h"><span>open</span><span>{ itoa(len(p.Open)) }</span></h2> | 27 | + | <h2 class="rule-h"><span>Open</span><span class="count">{ itoa(len(p.Open)) }</span></h2> |
| 28 | @mrRows(p.Open, "Open merge requests") | 28 | @mrRows(p.Open, "Open merge requests") | ||
| 29 | if len(p.Open) == 0 { | 29 | if len(p.Open) == 0 { | ||
| 30 | <p class="lede dim">no open merge requests.</p> | 30 | <p class="lede dim">no open merge requests.</p> | ||
| @@ -33,7 +33,7 @@ templ MRList(l Layout, p MRListPage) { | |||||
| 33 | <p class="note">{ p.OpenNote }</p> | 33 | <p class="note">{ p.OpenNote }</p> | ||
| 34 | } | 34 | } | ||
| 35 | } | 35 | } | ||
| 36 | - | <h2 class="rule-h"><span>merged and closed</span><span>{ itoa(len(p.Done)) }</span></h2> | 36 | + | <h2 class="rule-h"><span>Merged and closed</span><span class="count">{ itoa(len(p.Done)) }</span></h2> |
| 37 | @mrRows(p.Done, "Merged and closed merge requests") | 37 | @mrRows(p.Done, "Merged and closed merge requests") | ||
| 38 | if len(p.Done) == 0 { | 38 | if len(p.Done) == 0 { | ||
| 39 | <p class="lede dim">nothing merged or closed yet.</p> | 39 | <p class="lede dim">nothing merged or closed yet.</p> | ||
| @@ -41,14 +41,14 @@ templ MRList(l Layout, p MRListPage) { | |||||
| 41 | if p.Older != "" || p.Newest != "" { | 41 | if p.Older != "" || p.Newest != "" { | ||
| 42 | <nav class="pager" aria-label="Pages"> | 42 | <nav class="pager" aria-label="Pages"> | ||
| 43 | if p.Newest != "" { | 43 | if p.Newest != "" { | ||
| 44 | - | <a href={ templ.SafeURL(p.Newest) }>← newest</a> | 44 | + | <a class="btn" href={ templ.SafeURL(p.Newest) }>← Newest</a> |
| 45 | } else { | 45 | } else { | ||
| 46 | - | <span class="mute">← newest</span> | 46 | + | <span class="btn" aria-disabled="true">← Newest</span> |
| 47 | } | 47 | } | ||
| 48 | if p.Older != "" { | 48 | if p.Older != "" { | ||
| 49 | - | <a href={ templ.SafeURL(p.Older) } rel="next">older →</a> | 49 | + | <a class="btn" href={ templ.SafeURL(p.Older) } rel="next">Older →</a> |
| 50 | } else { | 50 | } else { | ||
| 51 | - | <span class="mute">older →</span> | 51 | + | <span class="btn" aria-disabled="true">Older →</span> |
| 52 | } | 52 | } | ||
| 53 | </nav> | 53 | </nav> | ||
| 54 | } | 54 | } | ||
| @@ -80,7 +80,7 @@ templ mrRows(rows []MRRow, label string) { | |||||
| 80 | 80 | ||||
| 81 | templ mrListRail(p MRListPage) { | 81 | templ mrListRail(p MRListPage) { | ||
| 82 | <section aria-labelledby="r-states"> | 82 | <section aria-labelledby="r-states"> | ||
| 83 | - | <h2 id="r-states">states</h2> | 83 | + | <h2 id="r-states">States</h2> |
| 84 | <dl class="kv"> | 84 | <dl class="kv"> | ||
| 85 | <dt>open</dt> | 85 | <dt>open</dt> | ||
| 86 | <dd>awaits review</dd> | 86 | <dd>awaits review</dd> | ||
| @@ -91,7 +91,7 @@ templ mrListRail(p MRListPage) { | |||||
| 91 | </dl> | 91 | </dl> | ||
| 92 | </section> | 92 | </section> | ||
| 93 | <section aria-labelledby="r-new"> | 93 | <section aria-labelledby="r-new"> | ||
| 94 | - | <h2 id="r-new">open one</h2> | 94 | + | <h2 id="r-new">Open one</h2> |
| 95 | if len(p.Branches) > 0 { | 95 | if len(p.Branches) > 0 { | ||
| 96 | <p class="dim">compare a branch with { p.Default }:</p> | 96 | <p class="dim">compare a branch with { p.Default }:</p> | ||
| 97 | <nav class="outline" aria-label="Branches"> | 97 | <nav class="outline" aria-label="Branches"> | ||
| @@ -166,9 +166,9 @@ templ MergeRequest(l Layout, p MRPage) { | |||||
| 166 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | 166 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | ||
| 167 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | 167 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | ||
| 168 | <span class="grow"></span> | 168 | <span class="grow"></span> | ||
| 169 | - | @viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref) | 169 | + | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) |
| 170 | - | @viewLink(p.DiffView, "split", "[d] split", "d", p.SplitHref) | 170 | + | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) |
| 171 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 171 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd></button> |
| 172 | </div> | 172 | </div> | ||
| 173 | if p.Invisible { | 173 | if p.Invisible { | ||
| 174 | <p class="notice"> | 174 | <p class="notice"> | ||
| @@ -197,7 +197,7 @@ templ MergeRequest(l Layout, p MRPage) { | |||||
| 197 | <p class="note">no changed files.</p> | 197 | <p class="note">no changed files.</p> | ||
| 198 | } | 198 | } | ||
| 199 | </div> | 199 | </div> | ||
| 200 | - | <h2 class="rule-h" id="comments"><span>comments</span><span>{ itoa(p.NUnresolved) } open / { itoa(p.NComments) }</span></h2> | 200 | + | <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(p.NUnresolved) } open / { itoa(p.NComments) }</span></h2> |
| 201 | for _, c := range p.Loose { | 201 | for _, c := range p.Loose { | ||
| 202 | @commentView(c, "") | 202 | @commentView(c, "") | ||
| 203 | } | 203 | } | ||
| @@ -257,7 +257,7 @@ templ commentView(c CommentView, suffix string) { | |||||
| 257 | if c.Resolve != nil { | 257 | if c.Resolve != nil { | ||
| 258 | <form class="c-act" method="post" action={ templ.SafeURL(c.Resolve.Action) }> | 258 | <form class="c-act" method="post" action={ templ.SafeURL(c.Resolve.Action) }> | ||
| 259 | <input type="hidden" name="resolved" value={ boolText(c.Resolve.Value) }/> | 259 | <input type="hidden" name="resolved" value={ boolText(c.Resolve.Value) }/> | ||
| 260 | - | <button type="submit">{ c.Resolve.Label }</button> | 260 | + | <button type="submit" class="btn">{ c.Resolve.Label }</button> |
| 261 | </form> | 261 | </form> | ||
| 262 | } | 262 | } | ||
| 263 | </div> | 263 | </div> | ||
| @@ -291,9 +291,9 @@ templ commentForm(f CommentForm, suffix string) { | |||||
| 291 | <input type="hidden" name="side" value={ f.Side }/> | 291 | <input type="hidden" name="side" value={ f.Side }/> | ||
| 292 | } | 292 | } | ||
| 293 | <div class="f-btns"> | 293 | <div class="f-btns"> | ||
| 294 | - | <button type="submit" class="f-main">[comment]</button> | 294 | + | <button type="submit" class="btn primary">Comment</button> |
| 295 | if f.Cancel != "" { | 295 | if f.Cancel != "" { | ||
| 296 | - | <a href={ templ.SafeURL(f.Cancel) }>[cancel]</a> | 296 | + | <a class="btn" href={ templ.SafeURL(f.Cancel) }>Cancel</a> |
| 297 | } | 297 | } | ||
| 298 | </div> | 298 | </div> | ||
| 299 | </form> | 299 | </form> | ||
| @@ -304,12 +304,12 @@ templ mrRail(p MRPage) { | |||||
| 304 | @mergeBox(p) | 304 | @mergeBox(p) | ||
| 305 | } | 305 | } | ||
| 306 | <section aria-labelledby="r-checks"> | 306 | <section aria-labelledby="r-checks"> | ||
| 307 | - | <h2 id="r-checks">checks <span>0</span></h2> | 307 | + | <h2 id="r-checks">Checks <span class="count">0</span></h2> |
| 308 | <p class="dim">no checks</p> | 308 | <p class="dim">no checks</p> | ||
| 309 | <p class="mute">there is no CI yet: nothing blocks the merge.</p> | 309 | <p class="mute">there is no CI yet: nothing blocks the merge.</p> | ||
| 310 | </section> | 310 | </section> | ||
| 311 | <section aria-labelledby="r-settings"> | 311 | <section aria-labelledby="r-settings"> | ||
| 312 | - | <h2 id="r-settings">settings</h2> | 312 | + | <h2 id="r-settings">Settings</h2> |
| 313 | <dl class="kv"> | 313 | <dl class="kv"> | ||
| 314 | <dt>modes</dt> | 314 | <dt>modes</dt> | ||
| 315 | <dd>{ strings.Join(p.Modes, ", ") }</dd> | 315 | <dd>{ strings.Join(p.Modes, ", ") }</dd> | ||
| @@ -324,7 +324,7 @@ templ mrRail(p MRPage) { | |||||
| 324 | </dl> | 324 | </dl> | ||
| 325 | </section> | 325 | </section> | ||
| 326 | <section aria-labelledby="r-commits"> | 326 | <section aria-labelledby="r-commits"> | ||
| 327 | - | <h2 id="r-commits">commits <span>{ itoa(len(p.Commits)) }</span></h2> | 327 | + | <h2 id="r-commits">Commits <span class="count">{ itoa(len(p.Commits)) }</span></h2> |
| 328 | <ul class="pcommits"> | 328 | <ul class="pcommits"> | ||
| 329 | for _, c := range p.Commits { | 329 | for _, c := range p.Commits { | ||
| 330 | <li data-new?={ c.New }> | 330 | <li data-new?={ c.New }> | ||
| @@ -350,7 +350,7 @@ templ mrRail(p MRPage) { | |||||
| 350 | } | 350 | } | ||
| 351 | </section> | 351 | </section> | ||
| 352 | <section aria-labelledby="r-files"> | 352 | <section aria-labelledby="r-files"> | ||
| 353 | - | <h2 id="r-files">files <span>{ itoa(len(p.Files)) }</span></h2> | 353 | + | <h2 id="r-files">Files <span class="count">{ itoa(len(p.Files)) }</span></h2> |
| 354 | @FileList(p.Files) | 354 | @FileList(p.Files) | ||
| 355 | </section> | 355 | </section> | ||
| 356 | } | 356 | } | ||
| @@ -360,7 +360,7 @@ templ mrRail(p MRPage) { | |||||
| 360 | // the other things one does with the request. | 360 | // the other things one does with the request. | ||
| 361 | templ mergeBox(p MRPage) { | 361 | templ mergeBox(p MRPage) { | ||
| 362 | <section class="decide" aria-labelledby="r-merge" data-state="open"> | 362 | <section class="decide" aria-labelledby="r-merge" data-state="open"> | ||
| 363 | - | <h2 id="r-merge">merge</h2> | 363 | + | <h2 id="r-merge">Merge</h2> |
| 364 | <dl class="kv"> | 364 | <dl class="kv"> | ||
| 365 | <dt>head</dt> | 365 | <dt>head</dt> | ||
| 366 | <dd> | 366 | <dd> | ||
| @@ -386,7 +386,7 @@ templ mergeBox(p MRPage) { | |||||
| 386 | <label class="d-check"><input type="checkbox" name="delete_branch" value="1"/> delete { p.Source } after the merge</label> | 386 | <label class="d-check"><input type="checkbox" name="delete_branch" value="1"/> delete { p.Source } after the merge</label> | ||
| 387 | } | 387 | } | ||
| 388 | <div class="d-btns"> | 388 | <div class="d-btns"> | ||
| 389 | - | <button type="submit" class="d-main">[merge]</button> | 389 | + | <button type="submit" class="btn primary">Merge</button> |
| 390 | </div> | 390 | </div> | ||
| 391 | </form> | 391 | </form> | ||
| 392 | } | 392 | } | ||
| @@ -394,11 +394,11 @@ templ mergeBox(p MRPage) { | |||||
| 394 | if p.Box.CanMarkRead { | 394 | if p.Box.CanMarkRead { | ||
| 395 | <form method="post" action={ templ.SafeURL(p.Box.ReadAction) }> | 395 | <form method="post" action={ templ.SafeURL(p.Box.ReadAction) }> | ||
| 396 | <input type="hidden" name="head_id" value={ p.Box.Head }/> | 396 | <input type="hidden" name="head_id" value={ p.Box.Head }/> | ||
| 397 | - | <button type="submit" title="the next visit can show only what changed after this head">[mark read]</button> | 397 | + | <button type="submit" title="the next visit can show only what changed after this head" class="btn">Mark read</button> |
| 398 | </form> | 398 | </form> | ||
| 399 | } | 399 | } | ||
| 400 | <form method="post" action={ templ.SafeURL(p.Box.CloseAction) }> | 400 | <form method="post" action={ templ.SafeURL(p.Box.CloseAction) }> | ||
| 401 | - | <button type="submit" title="close without merging; the branch is kept">[close]</button> | 401 | + | <button type="submit" title="close without merging; the branch is kept" class="btn danger">Close</button> |
| 402 | </form> | 402 | </form> | ||
| 403 | </div> | 403 | </div> | ||
| 404 | if !p.Box.HeadGone { | 404 | if !p.Box.HeadGone { | ||
| @@ -438,7 +438,7 @@ templ Compare(l Layout, p ComparePage) { | |||||
| 438 | <label for="mr-desc">description</label> | 438 | <label for="mr-desc">description</label> | ||
| 439 | <textarea id="mr-desc" name="description" rows="4" maxlength="8000" placeholder="plain text, optional"></textarea> | 439 | <textarea id="mr-desc" name="description" rows="4" maxlength="8000" placeholder="plain text, optional"></textarea> | ||
| 440 | <div class="f-btns"> | 440 | <div class="f-btns"> | ||
| 441 | - | <button type="submit" class="f-main">[create merge request]</button> | 441 | + | <button type="submit" class="btn primary">Create merge request</button> |
| 442 | </div> | 442 | </div> | ||
| 443 | </form> | 443 | </form> | ||
| 444 | } | 444 | } | ||
| @@ -446,9 +446,9 @@ templ Compare(l Layout, p ComparePage) { | |||||
| 446 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | 446 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | ||
| 447 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | 447 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | ||
| 448 | <span class="grow"></span> | 448 | <span class="grow"></span> | ||
| 449 | - | @viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref) | 449 | + | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) |
| 450 | - | @viewLink(p.DiffView, "split", "[d] split", "d", p.SplitHref) | 450 | + | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) |
| 451 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | 451 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn">Collapse all<kbd>x</kbd></button> |
| 452 | </div> | 452 | </div> | ||
| 453 | if p.Invisible { | 453 | if p.Invisible { | ||
| 454 | <p class="notice"> | 454 | <p class="notice"> | ||
| @@ -477,7 +477,7 @@ templ Compare(l Layout, p ComparePage) { | |||||
| 477 | 477 | ||||
| 478 | templ compareRail(p ComparePage) { | 478 | templ compareRail(p ComparePage) { | ||
| 479 | <section aria-labelledby="r-ahead"> | 479 | <section aria-labelledby="r-ahead"> | ||
| 480 | - | <h2 id="r-ahead">commits <span>{ itoa(len(p.Ahead)) }</span></h2> | 480 | + | <h2 id="r-ahead">Commits <span class="count">{ itoa(len(p.Ahead)) }</span></h2> |
| 481 | <ul class="pcommits"> | 481 | <ul class="pcommits"> | ||
| 482 | for _, c := range p.Ahead { | 482 | for _, c := range p.Ahead { | ||
| 483 | <li> | 483 | <li> | ||
| @@ -498,12 +498,12 @@ templ compareRail(p ComparePage) { | |||||
| 498 | </section> | 498 | </section> | ||
| 499 | if p.HasDiff { | 499 | if p.HasDiff { | ||
| 500 | <section aria-labelledby="r-files"> | 500 | <section aria-labelledby="r-files"> | ||
| 501 | - | <h2 id="r-files">files <span>{ itoa(len(p.Files)) }</span></h2> | 501 | + | <h2 id="r-files">Files <span class="count">{ itoa(len(p.Files)) }</span></h2> |
| 502 | @FileList(p.Files) | 502 | @FileList(p.Files) | ||
| 503 | </section> | 503 | </section> | ||
| 504 | } | 504 | } | ||
| 505 | <section aria-labelledby="r-cli"> | 505 | <section aria-labelledby="r-cli"> | ||
| 506 | - | <h2 id="r-cli">from a terminal</h2> | 506 | + | <h2 id="r-cli">From a terminal</h2> |
| 507 | @CloneBlock(p.CLI) | 507 | @CloneBlock(p.CLI) | ||
| 508 | </section> | 508 | </section> | ||
| 509 | } | 509 | } | ||
M internal/adapters/web/mr_post.go +1 -1
| @@ -109,7 +109,7 @@ func (s *Server) reject(ctx context.Context, w http.ResponseWriter, r *http.Requ | |||||
| 109 | if back == "" { | 109 | if back == "" { | ||
| 110 | back = "/" | 110 | back = "/" | ||
| 111 | } | 111 | } | ||
| 112 | - | l := s.stateLayout(title, itoa(status)+" · "+title, []Crumb{{Label: "-"}, {Label: title}}) | 112 | + | l := s.stateLayout(title, []Crumb{{Label: title, Current: true}}) |
| 113 | w.Header().Set("Cache-Control", "no-store") | 113 | w.Header().Set("Cache-Control", "no-store") | ||
| 114 | s.write(ctx, w, r, status, State(l, StatePage{ | 114 | s.write(ctx, w, r, status, State(l, StatePage{ | ||
| 115 | Code: itoa(status), Title: title, | 115 | Code: itoa(status), Title: title, | ||
M internal/adapters/web/mr_templ.go +44 -44 generated file: collapsed
M internal/adapters/web/overlays.templ +26 -26
| @@ -6,23 +6,22 @@ import "strings" | |||||
| 6 | 6 | ||||
| 7 | // OverlayHelp is the keyboard map panel. | 7 | // OverlayHelp is the keyboard map panel. | ||
| 8 | templ OverlayHelp() { | 8 | templ OverlayHelp() { | ||
| 9 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 9 | + | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 10 | - | <div class="panel-head"><span>keys</span><button type="button" hx-on:click="closest('dialog').close()">[esc] close</button></div> | 10 | + | <div class="panel-head"><h2>Keyboard shortcuts</h2><button type="button" class="panel-x" hx-on:click="closest('dialog').close()"><kbd>esc</kbd> close</button></div> |
| 11 | <dl class="keys"> | 11 | <dl class="keys"> | ||
| 12 | - | <div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move cursor down / up</dd></div> | 12 | + | <div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div> |
| 13 | - | <div><dt><kbd>enter</kbd></dt><dd>open row under cursor</dd></div> | 13 | + | <div><dt><kbd>?</kbd></dt><dd>this list</dd></div> |
| 14 | - | <div><dt><kbd>g</kbd> <kbd>g</kbd> / <kbd>G</kbd></dt><dd>first / last row</dd></div> | 14 | + | <div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move the row cursor down / up</dd></div> |
| 15 | - | <div><dt><kbd>/</kbd></dt><dd>focus the filter</dd></div> | 15 | + | <div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor</dd></div> |
| 16 | - | <div><dt><kbd>g h</kbd> <kbd>g t</kbd></dt><dd>go home / tree</dd></div> | 16 | + | <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div> |
| 17 | - | <div><dt><kbd>g c</kbd> <kbd>g r</kbd></dt><dd>go commits / refs</dd></div> | 17 | + | <div><dt><kbd>/</kbd></dt><dd>filter the list</dd></div> |
| 18 | - | <div><dt><kbd>b</kbd></dt><dd>{ "switch" } ref (branches, tags)</dd></div> | 18 | + | <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div> |
| 19 | - | <div><dt><kbd>:</kbd> <kbd>f</kbd></dt><dd>go to: section, ref, file, repo</dd></div> | 19 | + | <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd></dt><dd>tabs: summary, files, commits, branches & tags, merge requests</dd></div> |
| 20 | - | <div><dt><kbd>y</kbd></dt><dd>permalink: ref becomes the full sha</dd></div> | 20 | + | <div><dt><kbd>g h</kbd></dt><dd>home</dd></div> |
| 21 | - | <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd></dt><dd>repo sections (when shown)</dd></div> | 21 | + | <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div> |
| 22 | - | <div><dt><kbd>m</kbd> <kbd>g m</kbd></dt><dd>merge requests (when shown)</dd></div> | |||
| 23 | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse all</dd></div> | 22 | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse all</dd></div> | ||
| 24 | <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div> | 23 | <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div> | ||
| 25 | - | <div><dt><kbd>esc</kbd></dt><dd>close panel</dd></div> | 24 | + | <div><dt><kbd>esc</kbd></dt><dd>close a dialog, leave a field</dd></div> |
| 26 | </dl> | 25 | </dl> | ||
| 27 | </dialog> | 26 | </dialog> | ||
| 28 | } | 27 | } | ||
| @@ -30,22 +29,22 @@ templ OverlayHelp() { | |||||
| 30 | // OverlayRefs is the ref switcher. It exists only inside a repository. | 29 | // OverlayRefs is the ref switcher. It exists only inside a repository. | ||
| 31 | templ OverlayRefs(l Layout) { | 30 | templ OverlayRefs(l Layout) { | ||
| 32 | if l.Ref != "" { | 31 | if l.Ref != "" { | ||
| 33 | - | <dialog id="panel-refs" class="panel" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 32 | + | <dialog id="panel-refs" class="panel modal" aria-label="Switch ref" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 34 | <div class="panel-head"> | 33 | <div class="panel-head"> | ||
| 35 | - | <label for="refs-filter">ref></label> | 34 | + | <label for="refs-filter" class="sr">filter branches and tags</label> |
| 36 | <input | 35 | <input | ||
| 37 | id="refs-filter" | 36 | id="refs-filter" | ||
| 38 | type="search" | 37 | type="search" | ||
| 39 | - | placeholder="filter branches and tags" | 38 | + | placeholder="Switch branch or tag…" |
| 40 | autocomplete="off" | 39 | autocomplete="off" | ||
| 41 | data-pick | 40 | data-pick | ||
| 42 | hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))" | 41 | hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))" | ||
| 43 | /> | 42 | /> | ||
| 44 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 43 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"><kbd>esc</kbd></button> |
| 45 | </div> | 44 | </div> | ||
| 46 | <div class="panel-cols" data-picklist> | 45 | <div class="panel-cols" data-picklist> | ||
| 47 | <section aria-label="Branches"> | 46 | <section aria-label="Branches"> | ||
| 48 | - | <h2>branches <span class="mute">{ itoa(len(l.Heads)) }</span></h2> | 47 | + | <h2>Branches <span class="count">{ itoa(len(l.Heads)) }</span></h2> |
| 49 | <ul> | 48 | <ul> | ||
| 50 | for _, r := range l.Heads { | 49 | for _, r := range l.Heads { | ||
| 51 | @refItem(r) | 50 | @refItem(r) | ||
| @@ -53,7 +52,7 @@ templ OverlayRefs(l Layout) { | |||||
| 53 | </ul> | 52 | </ul> | ||
| 54 | </section> | 53 | </section> | ||
| 55 | <section aria-label="Tags"> | 54 | <section aria-label="Tags"> | ||
| 56 | - | <h2>tags <span class="mute">{ itoa(len(l.Tags)) }</span></h2> | 55 | + | <h2>Tags <span class="count">{ itoa(len(l.Tags)) }</span></h2> |
| 57 | <ul> | 56 | <ul> | ||
| 58 | for _, r := range l.Tags { | 57 | for _, r := range l.Tags { | ||
| 59 | @refItem(r) | 58 | @refItem(r) | ||
| @@ -87,13 +86,13 @@ templ refItem(r RefItem) { | |||||
| 87 | // OverlayCmd is the ":" command line: one filterable list mixing sections, | 86 | // OverlayCmd is the ":" command line: one filterable list mixing sections, | ||
| 88 | // refs, files of this directory and repos. | 87 | // refs, files of this directory and repos. | ||
| 89 | templ OverlayCmd(items []CmdItem) { | 88 | templ OverlayCmd(items []CmdItem) { | ||
| 90 | - | <dialog id="panel-cmd" class="panel" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | 89 | + | <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> |
| 91 | <div class="panel-head"> | 90 | <div class="panel-head"> | ||
| 92 | - | <label for="cmd-q" class="hi">:</label> | 91 | + | <label for="cmd-q" class="sr">go to</label> |
| 93 | <input | 92 | <input | ||
| 94 | id="cmd-q" | 93 | id="cmd-q" | ||
| 95 | type="search" | 94 | type="search" | ||
| 96 | - | placeholder="section, ref, file or repo" | 95 | + | placeholder="Go to a section, branch, tag, file or repository…" |
| 97 | autocomplete="off" | 96 | autocomplete="off" | ||
| 98 | spellcheck="false" | 97 | spellcheck="false" | ||
| 99 | aria-controls="cmd-list" | 98 | aria-controls="cmd-list" | ||
| @@ -106,12 +105,13 @@ templ OverlayCmd(items []CmdItem) { | |||||
| 106 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | 105 | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | ||
| 107 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }" | 106 | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }" | ||
| 108 | /> | 107 | /> | ||
| 109 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | 108 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"><kbd>esc</kbd></button> |
| 110 | </div> | 109 | </div> | ||
| 111 | - | <ul id="cmd-list" class="cmd-list"> | 110 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> |
| 112 | for i, c := range items { | 111 | for i, c := range items { | ||
| 113 | - | <li data-k={ strings.ToLower(c.Kind + " " + c.Label) } data-cursor?={ i == 0 }><a href={ templ.SafeURL(c.Href) } tabindex="-1"><span class="k">{ c.Kind }</span>{ c.Label }</a></li> | 112 | + | <li data-k={ strings.ToLower(c.Kind + " " + c.Label) } data-cursor?={ i == 0 }><a href={ templ.SafeURL(c.Href) } tabindex="-1"><span class="lbl">{ c.Label }</span><span class="k">{ c.Kind }</span></a></li> |
| 114 | } | 113 | } | ||
| 115 | </ul> | 114 | </ul> | ||
| 115 | + | <p class="panel-foot"><span><kbd>↑</kbd><kbd>↓</kbd> move</span><span><kbd>enter</kbd> open</span><span><kbd>esc</kbd> close</span></p> | |||
| 116 | </dialog> | 116 | </dialog> | ||
| 117 | } | 117 | } | ||
M internal/adapters/web/overlays_templ.go +19 -19 generated file: collapsed
M internal/adapters/web/pages.templ +16 -20
| @@ -6,12 +6,12 @@ package web | |||||
| 6 | templ Home(l Layout, p HomePage) { | 6 | templ Home(l Layout, p HomePage) { | ||
| 7 | @Shell(l, nil) { | 7 | @Shell(l, nil) { | ||
| 8 | <div class="filter"> | 8 | <div class="filter"> | ||
| 9 | - | <label for="home-filter">filter></label> | 9 | + | <label for="home-filter" class="sr">filter</label> |
| 10 | <input | 10 | <input | ||
| 11 | id="home-filter" | 11 | id="home-filter" | ||
| 12 | type="search" | 12 | type="search" | ||
| 13 | data-filter | 13 | data-filter | ||
| 14 | - | placeholder="name or description ( / )" | 14 | + | placeholder="Filter repositories…" |
| 15 | autocomplete="off" | 15 | autocomplete="off" | ||
| 16 | hx-on:input="let t = this.value.toLowerCase(); q('#repo-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#repo-rows > tr:not([hidden])')" | 16 | hx-on:input="let t = this.value.toLowerCase(); q('#repo-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#repo-rows > tr:not([hidden])')" | ||
| 17 | /> | 17 | /> | ||
| @@ -52,23 +52,22 @@ templ Summary(l Layout, p SummaryPage) { | |||||
| 52 | <p class="lede">{ p.Desc }</p> | 52 | <p class="lede">{ p.Desc }</p> | ||
| 53 | } | 53 | } | ||
| 54 | if p.Readme != nil { | 54 | if p.Readme != nil { | ||
| 55 | - | <h2 class="rule-h"><span>{ p.Readme.Name }</span></h2> | 55 | + | @ReadmeCard(*p.Readme) |
| 56 | - | @ReadmeView(*p.Readme) | |||
| 57 | } | 56 | } | ||
| 58 | } | 57 | } | ||
| 59 | } | 58 | } | ||
| 60 | 59 | ||||
| 61 | templ summaryRail(p SummaryPage) { | 60 | templ summaryRail(p SummaryPage) { | ||
| 62 | - | <section aria-labelledby="r-clone"><h2 id="r-clone">clone</h2>@CloneBlock(p.Clone) | 61 | + | <section aria-labelledby="r-clone"><h2 id="r-clone">Clone</h2>@CloneBlock(p.Clone) |
| 63 | </section> | 62 | </section> | ||
| 64 | - | <section aria-labelledby="r-facts"><h2 id="r-facts">facts</h2>@Facts(p.Facts) | 63 | + | <section aria-labelledby="r-facts"><h2 id="r-facts">About</h2>@Facts(p.Facts) |
| 65 | </section> | 64 | </section> | ||
| 66 | if len(p.Recent) > 0 { | 65 | if len(p.Recent) > 0 { | ||
| 67 | - | <section aria-labelledby="r-recent"><h2 id="r-recent">recent commits <a href={ templ.SafeURL(p.AllHref) }>all</a></h2>@CommitRows(p.Recent) | 66 | + | <section aria-labelledby="r-recent"><h2 id="r-recent">Recent commits <a href={ templ.SafeURL(p.AllHref) }>View all</a></h2>@CommitRows(p.Recent) |
| 68 | </section> | 67 | </section> | ||
| 69 | } | 68 | } | ||
| 70 | if len(p.Outline) > 0 && p.Readme != nil { | 69 | if len(p.Outline) > 0 && p.Readme != nil { | ||
| 71 | - | <section aria-labelledby="r-outline"><h2 id="r-outline">{ p.Readme.Name }</h2>@Outline(p.Outline) | 70 | + | <section aria-labelledby="r-outline"><h2 id="r-outline">On this page</h2>@Outline(p.Outline) |
| 72 | </section> | 71 | </section> | ||
| 73 | } | 72 | } | ||
| 74 | } | 73 | } | ||
| @@ -77,12 +76,12 @@ templ summaryRail(p SummaryPage) { | |||||
| 77 | templ Refs(l Layout, p RefsPage) { | 76 | templ Refs(l Layout, p RefsPage) { | ||
| 78 | @Shell(l, refsRail(p)) { | 77 | @Shell(l, refsRail(p)) { | ||
| 79 | <div class="filter"> | 78 | <div class="filter"> | ||
| 80 | - | <label for="ref-filter">filter></label> | 79 | + | <label for="ref-filter" class="sr">filter</label> |
| 81 | <input | 80 | <input | ||
| 82 | id="ref-filter" | 81 | id="ref-filter" | ||
| 83 | type="search" | 82 | type="search" | ||
| 84 | data-filter | 83 | data-filter | ||
| 85 | - | placeholder="branch or tag ( / )" | 84 | + | placeholder="Filter branches and tags…" |
| 86 | autocomplete="off" | 85 | autocomplete="off" | ||
| 87 | hx-on:input="let t = this.value.toLowerCase(); q('#ref-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | 86 | hx-on:input="let t = this.value.toLowerCase(); q('#ref-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | ||
| 88 | let first = q('#ref-rows > tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#ref-rows > tr')" | 87 | let first = q('#ref-rows > tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#ref-rows > tr')" | ||
| @@ -107,7 +106,7 @@ templ refRow(kind string, r RefRow) { | |||||
| 107 | <td class="c-name"> | 106 | <td class="c-name"> | ||
| 108 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | 107 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | ||
| 109 | if r.Default { | 108 | if r.Default { | ||
| 110 | - | <span class="tagged">*</span> | 109 | + | <span class="pill">default</span> |
| 111 | } | 110 | } | ||
| 112 | </td> | 111 | </td> | ||
| 113 | <td class="c-sha"> | 112 | <td class="c-sha"> | ||
| @@ -128,20 +127,17 @@ templ refRow(kind string, r RefRow) { | |||||
| 128 | 127 | ||||
| 129 | templ refsRail(p RefsPage) { | 128 | templ refsRail(p RefsPage) { | ||
| 130 | <section aria-labelledby="r-refs"> | 129 | <section aria-labelledby="r-refs"> | ||
| 131 | - | <h2 id="r-refs">refs</h2> | 130 | + | <h2 id="r-refs">Overview</h2> |
| 132 | @RefsKV(p) | 131 | @RefsKV(p) | ||
| 133 | </section> | 132 | </section> | ||
| 134 | - | <section aria-labelledby="r-legend"> | |||
| 135 | - | <h2 id="r-legend">legend</h2> | |||
| 136 | - | <dl class="kv"><dt>*</dt><dd>default branch</dd></dl> | |||
| 137 | - | </section> | |||
| 138 | } | 133 | } | ||
| 139 | 134 | ||||
| 140 | // State is a 404, 500 or empty-repository page inside the normal shell. | 135 | // State is a 404, 500 or empty-repository page inside the normal shell. | ||
| 141 | templ State(l Layout, p StatePage) { | 136 | templ State(l Layout, p StatePage) { | ||
| 142 | @Shell(l, nil) { | 137 | @Shell(l, nil) { | ||
| 143 | <section class="state" aria-labelledby="state-title"> | 138 | <section class="state" aria-labelledby="state-title"> | ||
| 144 | - | <h1 id="state-title"><span class="code">{ p.Code }</span> { p.Title }</h1> | 139 | + | <p class="eyebrow">{ p.Code }</p> |
| 140 | + | <h1 id="state-title">{ p.Title }</h1> | |||
| 145 | for _, line := range p.Lines { | 141 | for _, line := range p.Lines { | ||
| 146 | <p class="dim">{ line }</p> | 142 | <p class="dim">{ line }</p> | ||
| 147 | } | 143 | } | ||
| @@ -162,14 +158,14 @@ templ State(l Layout, p StatePage) { | |||||
| 162 | templ Login(l Layout, p LoginPage) { | 158 | templ Login(l Layout, p LoginPage) { | ||
| 163 | @Shell(l, nil) { | 159 | @Shell(l, nil) { | ||
| 164 | <section class="state" aria-labelledby="state-title"> | 160 | <section class="state" aria-labelledby="state-title"> | ||
| 165 | - | <h1 id="state-title"><span class="code">login</span> { p.Title }</h1> | 161 | + | <h1 id="state-title">{ p.Title }</h1> |
| 166 | for _, line := range p.Lines { | 162 | for _, line := range p.Lines { | ||
| 167 | <p class="dim">{ line }</p> | 163 | <p class="dim">{ line }</p> | ||
| 168 | } | 164 | } | ||
| 169 | <form class="state-act" method="post" action={ templ.SafeURL(p.Action) }> | 165 | <form class="state-act" method="post" action={ templ.SafeURL(p.Action) }> | ||
| 170 | - | <button type="submit">{ p.Button }</button> | 166 | + | <button type="submit" class="btn primary">{ p.Button }</button> |
| 171 | </form> | 167 | </form> | ||
| 172 | - | <p class="state-back"><a href="/">home</a></p> | 168 | + | <p class="state-back"><a href="/">Back to home</a></p> |
| 173 | </section> | 169 | </section> | ||
| 174 | } | 170 | } | ||
| 175 | } | 171 | } | ||
M internal/adapters/web/pages_templ.go +159 -189 generated file: collapsed
M internal/adapters/web/refs.go +1 -1
| @@ -165,7 +165,7 @@ func hasName(items []RefItem, name string) bool { | |||||
| 165 | return false | 165 | return false | ||
| 166 | } | 166 | } | ||
| 167 | 167 | ||||
| 168 | - | // pathCrumbs is the path in the top bar: repo, kind ("tree", "blob" or | 168 | + | // pathCrumbs is the path in the header: repo, kind ("tree", "blob" or |
| 169 | // "commits"), ref, then the segments of the path. The directories link to | 169 | // "commits"), ref, then the segments of the path. The directories link to | ||
| 170 | // their tree; the last segment links to the file for a blob, and to nothing | 170 | // their tree; the last segment links to the file for a blob, and to nothing | ||
| 171 | // for a history. | 171 | // for a history. | ||
M internal/adapters/web/refspage.go +1 -5
| @@ -26,11 +26,7 @@ func (s *Server) refs(ctx context.Context, r *http.Request) (templ.Component, in | |||||
| 26 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=refs") | 26 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=refs") | ||
| 27 | l.Crumbs[0].Current = false | 27 | l.Crumbs[0].Current = false | ||
| 28 | l.Crumbs = append(l.Crumbs, Crumb{Label: "refs", Current: true}) | 28 | l.Crumbs = append(l.Crumbs, Crumb{Label: "refs", Current: true}) | ||
| 29 | - | l.Status = Status{ | 29 | + | l.HasRail = true |
| 30 | - | Mode: "refs", Context: plural(len(p.Branches), "branch", "branches") + " · " + plural(len(p.Tags), "tag", "tags"), | |||
| 31 | - | Hints: "j/k move · enter open · / filter · ? help", Cursor: true, Count: len(p.Branches) + len(p.Tags), | |||
| 32 | - | } | |||
| 33 | - | l.HasRail, l.RailSum = true, "refs · legend" | |||
| 34 | return Refs(l, p), http.StatusOK, nil | 30 | return Refs(l, p), http.StatusOK, nil | ||
| 35 | } | 31 | } | ||
| 36 | 32 | ||||
M internal/adapters/web/repo.go +9 -9
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "context" | 6 | "context" | ||
| 7 | "slices" | 7 | "slices" | ||
| 8 | "sort" | 8 | "sort" | ||
| 9 | + | "strings" | |||
| 9 | "sync" | 10 | "sync" | ||
| 10 | "time" | 11 | "time" | ||
| 11 | 12 | ||||
| @@ -98,23 +99,23 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La | |||||
| 98 | l.Ref, l.RefPath = def, def | 99 | l.Ref, l.RefPath = def, def | ||
| 99 | l.SHA, l.Short = d.head.GetCommitId(), short(d.head.GetCommitId()) | 100 | l.SHA, l.Short = d.head.GetCommitId(), short(d.head.GetCommitId()) | ||
| 100 | } | 101 | } | ||
| 101 | - | mk := func(key, post, href, id string) Section { | 102 | + | mk := func(key, label, href, id string) Section { |
| 102 | - | return Section{Key: key, Post: post, Href: href, Current: section == id} | 103 | + | return Section{Key: key, Label: label, Href: href, Current: section == id} |
| 103 | } | 104 | } | ||
| 104 | l.Sections = []Section{ | 105 | l.Sections = []Section{ | ||
| 105 | - | mk("s", "ummary", repoHref(d.name()), "summary"), | 106 | + | mk("s", "Summary", repoHref(d.name()), "summary"), |
| 106 | - | mk("t", "ree", repoHref(d.name(), "tree", def), "tree"), | 107 | + | mk("t", "Files", repoHref(d.name(), "tree", def), "tree"), |
| 107 | - | mk("c", "ommits", repoHref(d.name(), "commits", def), "commits"), | 108 | + | mk("c", "Commits", repoHref(d.name(), "commits", def), "commits"), |
| 108 | - | mk("r", "efs", repoHref(d.name(), "refs"), "refs"), | 109 | + | mk("r", "Branches & tags", repoHref(d.name(), "refs"), "refs"), |
| 109 | } | 110 | } | ||
| 110 | if s.cfg.MergeRequests != nil { | 111 | if s.cfg.MergeRequests != nil { | ||
| 111 | - | l.Sections = append(l.Sections, mk("m", "erge requests", mrBase(d.name()), "merge-requests")) | 112 | + | l.Sections = append(l.Sections, mk("m", "Merge requests", mrBase(d.name()), "merge-requests")) |
| 112 | } | 113 | } | ||
| 113 | l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}} | 114 | l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}} | ||
| 114 | 115 | ||||
| 115 | l.Cmd = append(l.Cmd, CmdItem{Kind: "go", Label: "home", Href: "/"}) | 116 | l.Cmd = append(l.Cmd, CmdItem{Kind: "go", Label: "home", Href: "/"}) | ||
| 116 | for _, sec := range l.Sections { | 117 | for _, sec := range l.Sections { | ||
| 117 | - | l.Cmd = append(l.Cmd, CmdItem{Kind: "section", Label: sec.Key + sec.Post, Href: sec.Href}) | 118 | + | l.Cmd = append(l.Cmd, CmdItem{Kind: "section", Label: strings.ToLower(sec.Label), Href: sec.Href}) |
| 118 | } | 119 | } | ||
| 119 | if l.Ref != "" { | 120 | if l.Ref != "" { | ||
| 120 | for _, b := range d.branches { | 121 | for _, b := range d.branches { | ||
| @@ -233,7 +234,6 @@ func (s *Server) emptyState(ctx context.Context, d *repoData) (Layout, StatePage | |||||
| 233 | l.Title = d.name() + " · " + s.instance | 234 | l.Title = d.name() + " · " + s.instance | ||
| 234 | l.Sections = nil | 235 | l.Sections = nil | ||
| 235 | l.Cmd = slices.DeleteFunc(l.Cmd, func(c CmdItem) bool { return c.Kind == "section" }) | 236 | l.Cmd = slices.DeleteFunc(l.Cmd, func(c CmdItem) bool { return c.Kind == "section" }) | ||
| 236 | - | l.Status = Status{Mode: "empty", Context: "empty repository · 0 commits", Hints: "g h home · ? help"} | |||
| 237 | branch := d.repo.GetDefaultBranch() | 237 | branch := d.repo.GetDefaultBranch() | ||
| 238 | return l, StatePage{ | 238 | return l, StatePage{ | ||
| 239 | Code: "empty", Title: d.name() + " is empty", | 239 | Code: "empty", Title: d.name() + " is empty", | ||
M internal/adapters/web/session.go +3 -3
| @@ -251,12 +251,12 @@ func (s *Server) loginConfirm(w http.ResponseWriter, r *http.Request) { | |||||
| 251 | s.secretPage(w, r, http.StatusNotFound, s.loginRejectedPage()) | 251 | s.secretPage(w, r, http.StatusNotFound, s.loginRejectedPage()) | ||
| 252 | return | 252 | return | ||
| 253 | } | 253 | } | ||
| 254 | - | l := s.stateLayout("sign in", "sign in · confirm", []Crumb{{Label: "-"}, {Label: "sign in"}}) | 254 | + | l := s.stateLayout("sign in", []Crumb{{Label: "sign in", Current: true}}) |
| 255 | s.secretPage(w, r, http.StatusOK, Login(l, LoginPage{ | 255 | s.secretPage(w, r, http.StatusOK, Login(l, LoginPage{ | ||
| 256 | Title: "sign in as the owner", | 256 | Title: "sign in as the owner", | ||
| 257 | Lines: []string{"this browser will be signed in for 12 hours.", "only confirm if you ran `ermite web login` just now."}, | 257 | Lines: []string{"this browser will be signed in for 12 hours.", "only confirm if you ran `ermite web login` just now."}, | ||
| 258 | Action: loginPrefix + nonce, | 258 | Action: loginPrefix + nonce, | ||
| 259 | - | Button: "[sign in]", | 259 | + | Button: "Sign in", |
| 260 | })) | 260 | })) | ||
| 261 | } | 261 | } | ||
| 262 | 262 | ||||
| @@ -310,7 +310,7 @@ func (s *Server) logout(w http.ResponseWriter, r *http.Request) { | |||||
| 310 | 310 | ||||
| 311 | // stateOnly is a state page with a title and two lines, for the login flow. | 311 | // stateOnly is a state page with a title and two lines, for the login flow. | ||
| 312 | func (s *Server) stateOnly(crumb, code, title string, lines ...string) templ.Component { | 312 | func (s *Server) stateOnly(crumb, code, title string, lines ...string) templ.Component { | ||
| 313 | - | l := s.stateLayout(title, code+" · "+title, []Crumb{{Label: "-"}, {Label: crumb}}) | 313 | + | l := s.stateLayout(title, []Crumb{{Label: crumb, Current: true}}) |
| 314 | return State(l, StatePage{Code: code, Title: title, Lines: lines, Back: []Link{{Label: "home", Href: "/"}}}) | 314 | return State(l, StatePage{Code: code, Title: title, Lines: lines, Back: []Link{{Label: "home", Href: "/"}}}) | ||
| 315 | } | 315 | } | ||
| 316 | 316 | ||||
M internal/adapters/web/shell.templ +96 -84
| @@ -2,8 +2,9 @@ | |||||
| 2 | 2 | ||||
| 3 | package web | 3 | package web | ||
| 4 | 4 | ||||
| 5 | - | // Shell is the page: head, the two bars, the work area (content and rail), | 5 | + | // Shell is the page: head, the header, the work area (content and rail), the |
| 6 | - | // the overlays and the key map. The content is the children. | 6 | + | // footer, the hint badge, the overlays and the key map. The content is the |
| 7 | + | // children. | |||
| 7 | templ Shell(l Layout, rail templ.Component) { | 8 | templ Shell(l Layout, rail templ.Component) { | ||
| 8 | <!DOCTYPE html> | 9 | <!DOCTYPE html> | ||
| 9 | <html lang="en"> | 10 | <html lang="en"> | ||
| @@ -13,132 +14,143 @@ templ Shell(l Layout, rail templ.Component) { | |||||
| 13 | <meta name="color-scheme" content="light dark"/> | 14 | <meta name="color-scheme" content="light dark"/> | ||
| 14 | <title>{ l.Title }</title> | 15 | <title>{ l.Title }</title> | ||
| 15 | <link rel="icon" type="image/svg+xml" href={ templ.SafeURL(l.Assets.Icon) }/> | 16 | <link rel="icon" type="image/svg+xml" href={ templ.SafeURL(l.Assets.Icon) }/> | ||
| 17 | + | <link rel="preload" href={ templ.SafeURL(l.Assets.Font) } as="font" type="font/woff2" crossorigin/> | |||
| 16 | <link rel="stylesheet" href={ templ.SafeURL(l.Assets.CSS) }/> | 18 | <link rel="stylesheet" href={ templ.SafeURL(l.Assets.CSS) }/> | ||
| 17 | <script src={ l.Assets.HTMX } defer></script> | 19 | <script src={ l.Assets.HTMX } defer></script> | ||
| 18 | <script src={ l.Assets.Live } defer></script> | 20 | <script src={ l.Assets.Live } defer></script> | ||
| 19 | <script src={ l.Assets.SSE } defer></script> | 21 | <script src={ l.Assets.SSE } defer></script> | ||
| 20 | <script src={ l.Assets.App } defer></script> | 22 | <script src={ l.Assets.App } defer></script> | ||
| 21 | </head> | 23 | </head> | ||
| 22 | - | <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel=""> | 24 | + | <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel="" data-nav=""> |
| 23 | <a class="skip" href="#main">skip to content</a> | 25 | <a class="skip" href="#main">skip to content</a> | ||
| 24 | - | @Topbar(l) | 26 | + | @Header(l) |
| 25 | - | <div class={ "work", templ.KV("has-rail", l.HasRail), templ.KV("read", l.Read) }> | 27 | + | <div class={ "work", templ.KV("has-rail", l.HasRail && rail != nil) }> |
| 26 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | 28 | + | <main id="main" tabindex="-1"> |
| 27 | { children... } | 29 | { children... } | ||
| 28 | </main> | 30 | </main> | ||
| 29 | if l.HasRail && rail != nil { | 31 | if l.HasRail && rail != nil { | ||
| 30 | <aside class="rail" aria-label="Context"> | 32 | <aside class="rail" aria-label="Context"> | ||
| 31 | - | <details class="rail-d" open data-init="" hx-live="if (!data.init) { data.init = '1'; this.open = matchMedia('(min-width: 1100px)').matches }"> | 33 | + | @rail |
| 32 | - | <summary>{ l.RailSum }</summary> | |||
| 33 | - | @rail | |||
| 34 | - | </details> | |||
| 35 | </aside> | 34 | </aside> | ||
| 36 | } | 35 | } | ||
| 37 | </div> | 36 | </div> | ||
| 37 | + | @Footer(l) | |||
| 38 | + | @Hint(l) | |||
| 38 | @OverlayHelp() | 39 | @OverlayHelp() | ||
| 39 | @OverlayRefs(l) | 40 | @OverlayRefs(l) | ||
| 40 | @OverlayCmd(l.Cmd) | 41 | @OverlayCmd(l.Cmd) | ||
| 41 | - | @Statusbar(l) | |||
| 42 | @Keys() | 42 | @Keys() | ||
| 43 | </body> | 43 | </body> | ||
| 44 | </html> | 44 | </html> | ||
| 45 | } | 45 | } | ||
| 46 | 46 | ||||
| 47 | - | // Topbar is the top bar, neferbar style: the one accent block (instance) | 47 | + | // Header is the page header: the instance, the path, the ref switcher, the |
| 48 | - | // fading into the black bar, the path, the repo sections, the ref switcher. | 48 | + | // "go to" palette and the owner; inside a repository, the tabs below. |
| 49 | - | templ Topbar(l Layout) { | 49 | + | templ Header(l Layout) { |
| 50 | - | <header class="bar top"> | 50 | + | <header class="hdr"> |
| 51 | - | <a class="blk" href="/" data-g="h" title="home (g h)">{ l.Instance }</a><span class="fade" aria-hidden="true"></span> | 51 | + | <div class="hdr-top"> |
| 52 | - | <nav class="crumbs" aria-label="Path"> | 52 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> |
| 53 | - | <ol> | 53 | + | <span class="brand-mark" aria-hidden="true"></span> |
| 54 | - | for _, c := range l.Crumbs { | 54 | + | <span>{ l.Instance }</span> |
| 55 | - | <li> | 55 | + | </a> |
| 56 | - | if c.Href != "" { | 56 | + | if len(l.Crumbs) > 0 { |
| 57 | - | <a | 57 | + | <nav class="crumbs" aria-label="Path"> |
| 58 | - | href={ templ.SafeURL(c.Href) } | 58 | + | <ol> |
| 59 | - | if c.Current { | 59 | + | for _, c := range l.Crumbs { |
| 60 | - | aria-current="page" | 60 | + | <li> |
| 61 | + | if c.Href != "" { | |||
| 62 | + | <a | |||
| 63 | + | href={ templ.SafeURL(c.Href) } | |||
| 64 | + | if c.Current { | |||
| 65 | + | aria-current="page" | |||
| 66 | + | } | |||
| 67 | + | >{ c.Label }</a> | |||
| 68 | + | } else { | |||
| 69 | + | <span aria-current="page">{ c.Label }</span> | |||
| 61 | } | 70 | } | ||
| 62 | - | >{ c.Label }</a> | 71 | + | </li> |
| 63 | - | } else { | |||
| 64 | - | <span aria-current="page">{ c.Label }</span> | |||
| 65 | } | 72 | } | ||
| 66 | - | </li> | 73 | + | </ol> |
| 74 | + | </nav> | |||
| 75 | + | } | |||
| 76 | + | <div class="hdr-acts"> | |||
| 77 | + | if l.Ref != "" { | |||
| 78 | + | <button | |||
| 79 | + | type="button" | |||
| 80 | + | class="btn ref" | |||
| 81 | + | aria-haspopup="dialog" | |||
| 82 | + | aria-controls="panel-refs" | |||
| 83 | + | aria-keyshortcuts="b" | |||
| 84 | + | title="switch branch or tag (b)" | |||
| 85 | + | hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))" | |||
| 86 | + | > | |||
| 87 | + | <span class="ico i-branch" aria-hidden="true"></span> | |||
| 88 | + | <span class="ref-name">{ l.Ref }</span> | |||
| 89 | + | <span class="sha">{ l.Short }</span> | |||
| 90 | + | </button> | |||
| 67 | } | 91 | } | ||
| 68 | - | </ol> | 92 | + | <button |
| 69 | - | </nav> | 93 | + | type="button" |
| 94 | + | class="btn search" | |||
| 95 | + | aria-haspopup="dialog" | |||
| 96 | + | aria-controls="panel-cmd" | |||
| 97 | + | aria-keyshortcuts="Control+K :" | |||
| 98 | + | title="go to a section, ref, file or repo (ctrl k or :)" | |||
| 99 | + | hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))" | |||
| 100 | + | > | |||
| 101 | + | <span class="ico i-search" aria-hidden="true"></span> | |||
| 102 | + | <span class="search-label">Go to…</span> | |||
| 103 | + | <kbd>ctrl k</kbd> | |||
| 104 | + | </button> | |||
| 105 | + | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |||
| 106 | + | if SignedIn(ctx) { | |||
| 107 | + | <form class="owner" method="post" action="/-/logout"> | |||
| 108 | + | <span class="owner-badge" title="signed in as the owner">owner</span> | |||
| 109 | + | <button type="submit" class="btn" title="sign out of this browser">Sign out</button> | |||
| 110 | + | </form> | |||
| 111 | + | } | |||
| 112 | + | </div> | |||
| 113 | + | </div> | |||
| 70 | if len(l.Sections) > 0 { | 114 | if len(l.Sections) > 0 { | ||
| 71 | - | <nav class="secs" aria-label="Sections"> | 115 | + | <nav class="tabs" aria-label="Sections"> |
| 72 | <ul> | 116 | <ul> | ||
| 73 | for _, s := range l.Sections { | 117 | for _, s := range l.Sections { | ||
| 74 | <li> | 118 | <li> | ||
| 75 | <a | 119 | <a | ||
| 76 | href={ templ.SafeURL(s.Href) } | 120 | href={ templ.SafeURL(s.Href) } | ||
| 77 | data-key={ s.Key } | 121 | data-key={ s.Key } | ||
| 78 | - | aria-keyshortcuts={ "g " + s.Key } | 122 | + | aria-keyshortcuts={ s.Key } |
| 79 | if s.Current { | 123 | if s.Current { | ||
| 80 | aria-current="page" | 124 | aria-current="page" | ||
| 81 | } | 125 | } | ||
| 82 | - | >{ s.Key }{ s.Post }</a> | 126 | + | >{ s.Label }<kbd aria-hidden="true">{ s.Key }</kbd></a> |
| 83 | </li> | 127 | </li> | ||
| 84 | } | 128 | } | ||
| 85 | </ul> | 129 | </ul> | ||
| 86 | </nav> | 130 | </nav> | ||
| 87 | } | 131 | } | ||
| 88 | - | if l.Ref != "" { | |||
| 89 | - | <button | |||
| 90 | - | type="button" | |||
| 91 | - | class="ref" | |||
| 92 | - | aria-haspopup="dialog" | |||
| 93 | - | aria-controls="panel-refs" | |||
| 94 | - | aria-keyshortcuts="b" | |||
| 95 | - | title="switch ref (b)" | |||
| 96 | - | hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))" | |||
| 97 | - | >{ l.Ref }<span class="sha">{ "@" + l.Short }</span></button> | |||
| 98 | - | } | |||
| 99 | </header> | 132 | </header> | ||
| 100 | } | 133 | } | ||
| 101 | 134 | ||||
| 102 | - | // Statusbar is the bottom bar: mode, context, the ":" command prompt, the | |||
| 103 | - | // source link (AGPL §13), hints, live state and the cursor position. Plain | |||
| 104 | - | // text only: the top bar carries the accent. | |||
| 105 | - | templ Statusbar(l Layout) { | |||
| 106 | - | <footer class="bar status" aria-label="Status"> | |||
| 107 | - | <span class="mode">{ l.Status.Mode }</span> | |||
| 108 | - | <span class="st-ctx">{ l.Status.Context }</span> | |||
| 109 | - | <button | |||
| 110 | - | type="button" | |||
| 111 | - | class="st-cmd" | |||
| 112 | - | aria-haspopup="dialog" | |||
| 113 | - | aria-controls="panel-cmd" | |||
| 114 | - | aria-keyshortcuts=":" | |||
| 115 | - | hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))" | |||
| 116 | - | ><span class="hi">:</span> go to…</button> | |||
| 117 | - | if l.Status.Blob { | |||
| 118 | - | <span class="st-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : 'click a line number · shift-click extends'">click a line number · shift-click extends</span> | |||
| 119 | - | } | |||
| 120 | - | <span class="st-flash" aria-live="polite" :text="document.body.dataset.flash || ''"></span> | |||
| 121 | - | <span class="st-hints">{ l.Status.Hints }</span> | |||
| 122 | - | if SignedIn(ctx) { | |||
| 123 | - | <form class="st-owner" method="post" action="/-/logout"> | |||
| 124 | - | <span>owner</span> | |||
| 125 | - | <button type="submit" title="sign out of this browser">[sign out]</button> | |||
| 126 | - | </form> | |||
| 127 | - | } | |||
| 128 | - | @Footer(l) | |||
| 129 | - | @Live(l) | |||
| 130 | - | if l.Status.Cursor { | |||
| 131 | - | <span | |||
| 132 | - | class="st-pos num" | |||
| 133 | - | title="cursor position" | |||
| 134 | - | :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()" | |||
| 135 | - | >{ cursorText(l.Status.Count) }</span> | |||
| 136 | - | } | |||
| 137 | - | </footer> | |||
| 138 | - | } | |||
| 139 | - | ||||
| 140 | // Footer is the version and the link to the source of the running version, | 135 | // Footer is the version and the link to the source of the running version, | ||
| 141 | // which the AGPL §13 asks every user of the service to be offered. | 136 | // which the AGPL §13 asks every user of the service to be offered. | ||
| 142 | templ Footer(l Layout) { | 137 | templ Footer(l Layout) { | ||
| 143 | - | <span class="st-src"><span class="st-ver">ermite { l.Version } · </span><a href={ templ.SafeURL(l.SourceURL) } rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a></span> | 138 | + | <footer class="foot"> |
| 139 | + | <span>ermite { l.Version }</span> | |||
| 140 | + | <a href={ templ.SafeURL(l.SourceURL) } rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a> | |||
| 141 | + | <span class="foot-keys"><kbd>?</kbd> keyboard shortcuts</span> | |||
| 142 | + | </footer> | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // Hint is the floating badge at the bottom right. It shows only what the | |||
| 146 | + | // keyboard just did or what needs attention: the row position while j/k | |||
| 147 | + | // move, the selected lines of a file, a flash message (permalink copied) and | |||
| 148 | + | // a lost event stream. Empty, it takes no room. | |||
| 149 | + | templ Hint(l Layout) { | |||
| 150 | + | <div class="hint" role="status" aria-live="polite"> | |||
| 151 | + | <span class="hint-nav" :text="document.body.dataset.nav || ''"></span> | |||
| 152 | + | <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"></span> | |||
| 153 | + | <span class="hint-flash" :text="document.body.dataset.flash || ''"></span> | |||
| 154 | + | @Live(l) | |||
| 155 | + | </div> | |||
| 144 | } | 156 | } | ||
M internal/adapters/web/shell_templ.go +230 -274 generated file: collapsed
M internal/adapters/web/static/css/00-tokens.css +74 -46
| @@ -1,68 +1,96 @@ | |||||
| 1 | @layer reset, tokens, base, components, utilities; | 1 | @layer reset, tokens, base, components, utilities; | ||
| 2 | 2 | ||||
| 3 | - | /* Palette: the neferbar "mono" theme, black and grays only. | 3 | + | /* Fonts: Atkinson Hyperlegible Next for the interface, Atkinson Hyperlegible |
| 4 | - | Page surfaces follow the OS theme; the two bars stay black in both themes, | 4 | + | Mono for titles, code, shas, refs and keys. Both are embedded (static/fonts, |
| 5 | - | like neferbar under a light or a dark terminal. */ | 5 | + | SIL OFL 1.1), latin subset, variable weight. The asset loader turns the |
| 6 | + | relative font and icon references into hashed URLs. */ | |||
| 7 | + | @font-face { | |||
| 8 | + | font-family: "Atkinson Hyperlegible Next"; | |||
| 9 | + | src: url(fonts/atkinson-next.woff2) format("woff2"); | |||
| 10 | + | font-weight: 200 800; font-style: normal; font-display: swap; | |||
| 11 | + | } | |||
| 12 | + | @font-face { | |||
| 13 | + | font-family: "Atkinson Hyperlegible Next"; | |||
| 14 | + | src: url(fonts/atkinson-next-italic.woff2) format("woff2"); | |||
| 15 | + | font-weight: 200 800; font-style: italic; font-display: swap; | |||
| 16 | + | } | |||
| 17 | + | @font-face { | |||
| 18 | + | font-family: "Atkinson Hyperlegible Mono"; | |||
| 19 | + | src: url(fonts/atkinson-mono.woff2) format("woff2"); | |||
| 20 | + | font-weight: 200 800; font-style: normal; font-display: swap; | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | /* Palette: grays only, in four surface levels, so that blocks read apart | |||
| 24 | + | without hue. Dark: each level is lighter than the one below it; light: each | |||
| 25 | + | is whiter. Hue is kept for code, diffs and verdicts. */ | |||
| 6 | @layer tokens { | 26 | @layer tokens { | ||
| 7 | :root { | 27 | :root { | ||
| 8 | color-scheme: light dark; | 28 | color-scheme: light dark; | ||
| 9 | 29 | ||||
| 10 | - | /* page */ | 30 | + | /* surfaces, bottom to top */ |
| 11 | - | --paper: light-dark(#f2f2f2, #000000); | 31 | + | --bg-canvas: light-dark(#f3f3f3, #0c0c0c); /* the page */ |
| 12 | - | --paper-alt: light-dark(#e8e8e8, #0c0c0c); | 32 | + | --bg-inset: light-dark(#f8f8f8, #080808); /* code, fields: sunk into a surface */ |
| 13 | - | --row: light-dark(#dddddd, #1a1a1a); | 33 | + | --bg-surface:light-dark(#ffffff, #141414); /* cards, header, rail blocks */ |
| 14 | - | --sel: light-dark(#cbcbcb, #303030); | 34 | + | --bg-raised: light-dark(#ececec, #1d1d1d); /* hover, buttons, panel rows */ |
| 15 | - | --ink: light-dark(#161616, #d0d0d0); | 35 | + | --bg-strong: light-dark(#dedede, #292929); /* selected row, pressed button */ |
| 16 | - | --ink-hi: light-dark(#000000, #ffffff); | 36 | + | --bg-overlay:light-dark(#ffffff, #171717); /* dialogs */ |
| 17 | - | --ink-dim: light-dark(#4a4a4a, #9a9a9a); | 37 | + | --backdrop: light-dark(rgb(0 0 0 / .25), rgb(0 0 0 / .6)); |
| 18 | - | --ink-mute: light-dark(#5c5c5c, #8a8a8a); | |||
| 19 | - | --rule: light-dark(#bcbcbc, #303030); | |||
| 20 | - | --focus: light-dark(#000000, #ffffff); | |||
| 21 | 38 | ||||
| 22 | - | /* bars (neferbar mono.conf: background, foreground, color4 accent, color0 ink) */ | 39 | + | --border: light-dark(#dddddd, #242424); |
| 23 | - | --bar: #000000; | 40 | + | --border-strong: light-dark(#c2c2c2, #383838); |
| 24 | - | --bar-ink: #d0d0d0; | |||
| 25 | - | --bar-dim: #8a8a8a; | |||
| 26 | - | --accent: #c8c8c8; | |||
| 27 | - | --on-accent: #000000; | |||
| 28 | - | /* lib.sh fade(): four cells from the accent to the bar, each pulled halfway to the bar */ | |||
| 29 | - | --fade-1: #505050; | |||
| 30 | - | --fade-2: #3c3c3c; | |||
| 31 | - | --fade-3: #282828; | |||
| 32 | - | --fade-4: #141414; | |||
| 33 | 41 | ||||
| 34 | - | /* Colour, sparingly, from the owner's Despair theme: only code and diffs carry hue. | 42 | + | --ink: light-dark(#1b1b1b, #d6d6d6); |
| 35 | - | Dark values are Despair's own; light values are the same hues darkened to AA on --paper. */ | 43 | + | --ink-hi: light-dark(#000000, #fafafa); |
| 36 | - | --teal: light-dark(#2f6b6b, #7db5b5); /* Despair bright teal: keywords, additions */ | 44 | + | --ink-dim: light-dark(#4d4d4d, #a3a3a3); |
| 37 | - | --teal-2: light-dark(#355f5f, #5a8585); /* slate teal: types, numbers */ | 45 | + | --ink-mute: light-dark(#616161, #8c8c8c); |
| 38 | - | --red: light-dark(#a8404b, #d96c75); /* soft red: deletions */ | 46 | + | --focus: light-dark(#000000, #ffffff); |
| 39 | - | --rose: light-dark(#6b6570, #a7a2a9); /* rose quartz: strings */ | 47 | + | --on-ink: light-dark(#ffffff, #0c0c0c); /* text on a filled ink button */ |
| 48 | + | ||||
| 49 | + | /* Colour, sparingly, from the owner's Despair theme: only code, diffs and | |||
| 50 | + | verdicts carry hue. Light values are the same hues darkened to AA. */ | |||
| 51 | + | --teal: light-dark(#2f6b6b, #7db5b5); | |||
| 52 | + | --teal-2: light-dark(#355f5f, #5a8585); | |||
| 53 | + | --red: light-dark(#a8404b, #d96c75); | |||
| 54 | + | --rose: light-dark(#6b6570, #a7a2a9); | |||
| 40 | 55 | ||||
| 41 | --syn-plain: var(--ink); | 56 | --syn-plain: var(--ink); | ||
| 42 | --syn-keyword: var(--teal); | 57 | --syn-keyword: var(--teal); | ||
| 43 | --syn-string: var(--rose); | 58 | --syn-string: var(--rose); | ||
| 44 | - | --syn-comment: light-dark(#5c5f63, #6a6e73); | 59 | + | --syn-comment: light-dark(#5c5f63, #6e7277); |
| 45 | --syn-number: var(--teal-2); | 60 | --syn-number: var(--teal-2); | ||
| 46 | --syn-func: var(--ink-hi); | 61 | --syn-func: var(--ink-hi); | ||
| 47 | --syn-type: var(--teal-2); | 62 | --syn-type: var(--teal-2); | ||
| 48 | --syn-punct: var(--ink-dim); | 63 | --syn-punct: var(--ink-dim); | ||
| 49 | 64 | ||||
| 50 | - | /* diff rows: the line keeps the normal ink; the hue is in a faint row tint and the marker */ | 65 | + | --add: light-dark(#dcebeb, #111b1b); |
| 51 | - | --add: light-dark(#d8e5e5, #121919); | 66 | + | --del: light-dark(#f2dfe1, #231315); |
| 52 | - | --del: light-dark(#eedadc, #231113); | |||
| 53 | 67 | ||||
| 54 | - | /* one family (the neferbar font when installed), three sizes, two weights */ | 68 | + | --font: "Atkinson Hyperlegible Next", system-ui, sans-serif; |
| 55 | - | --font: "Adwaita Mono", "AdwaitaMono Nerd Font Mono", ui-monospace, "Cascadia Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace; | 69 | + | --mono: "Atkinson Hyperlegible Mono", ui-monospace, "Cascadia Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace; |
| 56 | - | --fs: 14px; | 70 | + | --fs: 15px; |
| 57 | --fs-code: 13px; | 71 | --fs-code: 13px; | ||
| 58 | - | --fs-s: 12px; | 72 | + | --fs-s: 13px; |
| 59 | - | --lh: 20px; | 73 | + | --fs-xs: 12px; |
| 60 | - | --lh-code: 18px; | 74 | + | --lh: 24px; |
| 75 | + | --lh-code: 20px; | |||
| 76 | + | ||||
| 77 | + | /* spacing scale */ | |||
| 78 | + | --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px; --s7: 48px; | |||
| 79 | + | --r: 8px; /* cards, dialogs */ | |||
| 80 | + | --r-s: 6px; /* buttons, fields */ | |||
| 81 | + | ||||
| 82 | + | --pad: var(--s5); | |||
| 83 | + | --stick: 0px; /* top offset of sticky headers inside the content */ | |||
| 84 | + | --measure: 80ch; /* reading width of prose */ | |||
| 85 | + | --rail: 340px; | |||
| 86 | + | --page: 1320px; | |||
| 87 | + | --shadow: 0 1px 2px rgb(0 0 0 / .06), 0 8px 32px rgb(0 0 0 / .18); | |||
| 61 | 88 | ||||
| 62 | - | --pad: 2ch; | 89 | + | /* names used by the code, diff and merge request sheets */ |
| 63 | - | --bar-h: 20px; /* one text line, like neferbar: no padding above or below */ | 90 | + | --paper: var(--bg-canvas); |
| 64 | - | --stick: var(--bar-h); /* top offset of sticky headers inside the content (0 in the app shell) */ | 91 | + | --paper-alt: var(--bg-raised); |
| 65 | - | --measure: 80ch; | 92 | + | --row: var(--bg-raised); |
| 66 | - | --rail: 40ch; | 93 | + | --sel: var(--bg-strong); |
| 94 | + | --rule: var(--border); | |||
| 67 | } | 95 | } | ||
| 68 | } | 96 | } | ||
M internal/adapters/web/static/css/10-base.css +38 -23
| @@ -1,56 +1,71 @@ | |||||
| 1 | @layer reset { | 1 | @layer reset { | ||
| 2 | - | *, *::before, *::after { box-sizing: border-box; border-radius: 0; } | 2 | + | *, *::before, *::after { box-sizing: border-box; } |
| 3 | html { -webkit-text-size-adjust: 100%; } | 3 | html { -webkit-text-size-adjust: 100%; } | ||
| 4 | body, h1, h2, h3, h4, p, ul, ol, dl, dd, figure, pre { margin: 0; } | 4 | body, h1, h2, h3, h4, p, ul, ol, dl, dd, figure, pre { margin: 0; } | ||
| 5 | ul, ol { padding: 0; list-style: none; } | 5 | ul, ol { padding: 0; list-style: none; } | ||
| 6 | table { border-collapse: collapse; border-spacing: 0; } | 6 | table { border-collapse: collapse; border-spacing: 0; } | ||
| 7 | th, td { padding: 0; text-align: left; font-weight: inherit; } | 7 | th, td { padding: 0; text-align: left; font-weight: inherit; } | ||
| 8 | - | button, input, select { font: inherit; color: inherit; letter-spacing: inherit; } | 8 | + | button, input, select, textarea { font: inherit; color: inherit; letter-spacing: inherit; } |
| 9 | button { background: none; border: 0; padding: 0; cursor: pointer; text-align: inherit; } | 9 | button { background: none; border: 0; padding: 0; cursor: pointer; text-align: inherit; } | ||
| 10 | - | input { background: transparent; border: 0; border-radius: 0; } | 10 | + | input { background: transparent; border: 0; } |
| 11 | summary { cursor: pointer; } | 11 | summary { cursor: pointer; } | ||
| 12 | dialog { color: inherit; border: 0; padding: 0; max-width: none; max-height: none; } | 12 | dialog { color: inherit; border: 0; padding: 0; max-width: none; max-height: none; } | ||
| 13 | img { max-width: 100%; } | 13 | img { max-width: 100%; } | ||
| 14 | } | 14 | } | ||
| 15 | 15 | ||||
| 16 | @layer base { | 16 | @layer base { | ||
| 17 | - | html { background: var(--paper); scrollbar-width: thin; scrollbar-color: var(--rule) transparent; } | 17 | + | /* a cross-document fade between pages, where the browser has it */ |
| 18 | + | @view-transition { navigation: auto; } | |||
| 19 | + | ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 120ms; } | |||
| 20 | + | @media (prefers-reduced-motion: reduce) { | |||
| 21 | + | @view-transition { navigation: none; } | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | html { background: var(--bg-canvas); scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; } | |||
| 18 | body { | 25 | body { | ||
| 19 | min-height: 100dvh; | 26 | min-height: 100dvh; | ||
| 20 | - | display: grid; | 27 | + | display: flex; flex-direction: column; |
| 21 | - | grid-template-rows: auto auto 1fr auto; | |||
| 22 | font: var(--fs) / var(--lh) var(--font); | 28 | font: var(--fs) / var(--lh) var(--font); | ||
| 23 | color: var(--ink); | 29 | color: var(--ink); | ||
| 24 | - | background: var(--paper); | 30 | + | background: var(--bg-canvas); |
| 25 | - | font-variant-ligatures: none; | |||
| 26 | tab-size: 4; | 31 | tab-size: 4; | ||
| 32 | + | -webkit-font-smoothing: antialiased; | |||
| 27 | } | 33 | } | ||
| 28 | - | h1, h2, h3, h4 { font-size: inherit; font-weight: bold; } | 34 | + | h1, h2, h3, h4 { font-family: var(--mono); font-weight: 700; color: var(--ink-hi); letter-spacing: -.01em; } |
| 29 | - | b, strong { font-weight: bold; } | 35 | + | h1 { font-size: 22px; line-height: 30px; } |
| 30 | - | em, i { font-style: normal; text-decoration: underline; text-underline-offset: 3px; } | 36 | + | h2 { font-size: 17px; line-height: 26px; } |
| 37 | + | h3, h4 { font-size: var(--fs); } | |||
| 38 | + | b, strong { font-weight: 700; } | |||
| 31 | small { font-size: var(--fs-s); } | 39 | small { font-size: var(--fs-s); } | ||
| 32 | - | code, pre, kbd, samp { font: inherit; } | 40 | + | code, pre, kbd, samp { font-family: var(--mono); } |
| 33 | - | a { color: inherit; text-decoration: underline; text-decoration-color: var(--rule); text-underline-offset: 3px; } | 41 | + | a { color: inherit; text-decoration: underline; text-decoration-color: var(--border-strong); text-underline-offset: 3px; } |
| 34 | - | a:hover { text-decoration-color: currentColor; } | 42 | + | a:hover { text-decoration-color: currentColor; color: var(--ink-hi); } |
| 35 | - | hr { border: 0; border-top: 1px solid var(--rule); margin: var(--lh) 0; } | 43 | + | hr { border: 0; border-top: 1px solid var(--border); margin: var(--s5) 0; } |
| 36 | - | ::selection { background: var(--ink); color: var(--paper); } | 44 | + | ::selection { background: var(--ink-hi); color: var(--on-ink); } |
| 37 | 45 | ||||
| 38 | :focus { outline: none; } | 46 | :focus { outline: none; } | ||
| 39 | - | :focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; } | 47 | + | :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 3px; } |
| 40 | 48 | ||||
| 41 | [hidden] { display: none !important; } | 49 | [hidden] { display: none !important; } | ||
| 42 | 50 | ||||
| 51 | + | kbd { | |||
| 52 | + | display: inline-block; min-width: 20px; padding: 0 5px; | |||
| 53 | + | font: 600 var(--fs-xs) / 18px var(--mono); text-align: center; color: var(--ink-dim); | |||
| 54 | + | background: var(--bg-canvas); border: 1px solid var(--border-strong); border-bottom-width: 2px; border-radius: 4px; | |||
| 55 | + | } | |||
| 56 | + | ||||
| 43 | .skip { | 57 | .skip { | ||
| 44 | - | position: absolute; left: 0; top: 0; z-index: 30; padding: 0 1ch; | 58 | + | position: absolute; left: var(--s2); top: var(--s2); z-index: 30; padding: var(--s1) var(--s3); |
| 45 | - | background: var(--ink); color: var(--paper); | 59 | + | background: var(--ink-hi); color: var(--on-ink); border-radius: var(--r-s); |
| 46 | - | transform: translateY(-100%); | 60 | + | transform: translateY(-200%); |
| 47 | } | 61 | } | ||
| 48 | .skip:focus { transform: none; } | 62 | .skip:focus { transform: none; } | ||
| 49 | 63 | ||||
| 50 | - | main { padding: 0 var(--pad) var(--lh); min-width: 0; } | 64 | + | main { min-width: 0; } |
| 65 | + | main:focus-visible { outline: none; } | |||
| 51 | 66 | ||||
| 52 | @media (prefers-reduced-motion: no-preference) { | 67 | @media (prefers-reduced-motion: no-preference) { | ||
| 53 | - | a, button, tr, li, summary { transition: background-color 90ms linear, opacity 90ms linear; } | 68 | + | a, button, tr, li, summary, .btn { transition: background-color 100ms ease, border-color 100ms ease, color 100ms ease; } |
| 54 | } | 69 | } | ||
| 55 | } | 70 | } | ||
| 56 | 71 | ||||
| @@ -60,5 +75,5 @@ | |||||
| 60 | .nowrap { white-space: nowrap; } | 75 | .nowrap { white-space: nowrap; } | ||
| 61 | .num { text-align: right; font-variant-numeric: tabular-nums; } | 76 | .num { text-align: right; font-variant-numeric: tabular-nums; } | ||
| 62 | .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } | 77 | .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } | ||
| 63 | - | .rule { border-top: 1px solid var(--rule); } | 78 | + | .rule { border-top: 1px solid var(--border); } |
| 64 | } | 79 | } | ||
M internal/adapters/web/static/css/20-shell.css +170 -110
| @@ -1,126 +1,186 @@ | |||||
| 1 | - | @layer base { | |||
| 2 | - | body { grid-template-rows: auto 1fr auto; } | |||
| 3 | - | } | |||
| 4 | - | ||||
| 5 | @layer components { | 1 | @layer components { | ||
| 6 | - | /* neferbar: black bars; one accent block (top left) with a 4-cell stepped fade. | 2 | + | /* buttons: one shape for links and buttons that act */ |
| 7 | - | The fade is four hard-edged 1ch cells (not a blend): it is the bar's own glyph-grid look. */ | 3 | + | .btn { |
| 8 | - | .bar { | 4 | + | display: inline-flex; align-items: center; gap: var(--s2); white-space: nowrap; |
| 9 | - | display: flex; align-items: stretch; column-gap: 1ch; | 5 | + | height: 32px; padding: 0 var(--s3); |
| 10 | - | height: var(--bar-h); line-height: var(--bar-h); | 6 | + | font: 600 var(--fs-s) / 1 var(--font); color: var(--ink); text-decoration: none; |
| 11 | - | background: var(--bar); color: var(--bar-ink); | 7 | + | background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: var(--r-s); |
| 12 | - | white-space: nowrap; overflow: hidden; | 8 | + | cursor: pointer; |
| 13 | - | position: sticky; z-index: 20; | 9 | + | &:hover { background: var(--bg-strong); color: var(--ink-hi); text-decoration: none; } |
| 14 | - | & a { text-decoration: none; } | 10 | + | & kbd { margin-right: -4px; } |
| 15 | - | & a:hover { text-decoration: underline; } | 11 | + | &.primary { background: var(--ink-hi); color: var(--on-ink); border-color: var(--ink-hi); } |
| 16 | - | & :focus-visible { outline-color: var(--accent); outline-offset: -2px; } | 12 | + | &.primary:hover { background: var(--ink); border-color: var(--ink); color: var(--on-ink); } |
| 17 | - | & .blk { background: var(--accent); color: var(--on-accent); font-weight: bold; padding: 0 1ch; } | 13 | + | &.danger:hover { color: var(--red); border-color: var(--red); } |
| 18 | - | & .fade { | 14 | + | &.icon { width: 32px; justify-content: center; padding: 0; } |
| 19 | - | flex: none; width: 4ch; margin-left: -1ch; | 15 | + | &[aria-disabled="true"] { opacity: .45; pointer-events: none; } |
| 20 | - | background: linear-gradient(to right, | 16 | + | &[aria-current="true"], &[aria-pressed="true"] { background: var(--bg-strong); color: var(--ink-hi); border-color: var(--ink-mute); } |
| 21 | - | var(--fade-1) 0 25%, var(--fade-2) 25% 50%, var(--fade-3) 50% 75%, var(--fade-4) 75%); | |||
| 22 | - | } | |||
| 23 | } | 17 | } | ||
| 18 | + | /* icons: SVG files used as masks, so they take the colour of the text */ | |||
| 19 | + | .ico { display: inline-block; width: 16px; height: 16px; flex: none; background: currentColor; mask: var(--i) center / contain no-repeat; } | |||
| 20 | + | .i-branch { --i: url(icons/branch.svg); } | |||
| 21 | + | .i-search { --i: url(icons/search.svg); } | |||
| 22 | + | .i-file { --i: url(icons/file.svg); } | |||
| 24 | 23 | ||||
| 25 | - | .top { | 24 | + | .pill { |
| 26 | - | top: 0; | 25 | + | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); |
| 27 | - | & .crumbs ol { display: flex; } | 26 | + | font: 600 var(--fs-xs) / 1 var(--mono); color: var(--ink-dim); vertical-align: 1px; |
| 28 | - | & .crumbs li + li::before { content: "/"; color: var(--bar-dim); padding: 0 .5ch; } | 27 | + | background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: 999px; white-space: nowrap; |
| 29 | - | & .crumbs [aria-current] { color: #fff; font-weight: bold; } | 28 | + | &.tag::before { content: ""; width: 6px; height: 6px; margin-right: 6px; border: 1.5px solid currentColor; border-radius: 1px; transform: rotate(45deg); } |
| 30 | - | & .secs { margin-left: 2ch; overflow: hidden; } | 29 | + | &.new { color: var(--teal); border-color: currentColor; } |
| 31 | - | & .secs ul { display: flex; } | 30 | + | } |
| 32 | - | & .secs li + li::before { content: "/"; color: var(--bar-dim); } | 31 | + | .count { |
| 33 | - | & .secs a { display: inline-block; padding: 0 1ch; color: var(--bar-dim); } | 32 | + | display: inline-block; min-width: 22px; padding: 0 7px; margin-left: var(--s1); |
| 34 | - | & .secs a:hover { color: var(--bar-ink); } | 33 | + | font: 600 var(--fs-xs) / 20px var(--font); text-align: center; color: var(--ink-dim); |
| 35 | - | & .secs a::first-letter { text-decoration: underline; } | 34 | + | background: var(--bg-raised); border-radius: 999px; vertical-align: 1px; |
| 36 | - | & .secs a[aria-current] { color: #fff; font-weight: bold; } | |||
| 37 | - | & .secs a[aria-current]::before { content: "▸ "; } | |||
| 38 | - | & .ref { flex: none; margin-left: auto; padding-right: 1ch; color: #fff; font-weight: bold; } | |||
| 39 | - | & .ref .sha { font-weight: normal; color: var(--bar-dim); } | |||
| 40 | - | & .ref:hover { text-decoration: underline; } | |||
| 41 | } | 35 | } | ||
| 42 | 36 | ||||
| 43 | - | .status { | 37 | + | /* header: identity, path and actions on top, the repository tabs below. |
| 44 | - | bottom: 0; font-size: var(--fs-s); | 38 | + | --hdr-h is its height, the offset of the sticky bars of the diffs. */ |
| 45 | - | padding: 0 1ch; | 39 | + | body { --hdr-h: 57px; } |
| 46 | - | & > span, & .st-cmd { color: var(--bar-dim); } | 40 | + | body:has(.hdr .tabs) { --hdr-h: 101px; } |
| 47 | - | & .mode { color: #fff; font-weight: bold; } | 41 | + | .hdr { |
| 48 | - | & .st-ctx { color: var(--bar-ink); } | 42 | + | position: sticky; top: 0; z-index: 20; |
| 49 | - | & .st-cmd { flex: none; } | 43 | + | background: var(--bg-surface); border-bottom: 1px solid var(--border); |
| 50 | - | & .st-cmd .hi, & .st-cmd:hover { color: #fff; font-weight: bold; } | 44 | + | } |
| 51 | - | & .st-sel { color: var(--bar-ink); } | 45 | + | .hdr-top { |
| 52 | - | & .st-flash:empty { display: none; } | 46 | + | display: flex; align-items: center; gap: var(--s4); |
| 53 | - | & .st-flash { color: #fff; font-weight: bold; } | 47 | + | max-width: var(--page); margin: 0 auto; height: 56px; padding: 0 var(--pad); |
| 54 | - | & .st-hints { margin-left: auto; overflow: hidden; text-overflow: ellipsis; min-width: 0; } | 48 | + | } |
| 55 | - | & .st-live::before { content: "○ "; } | 49 | + | .brand { |
| 56 | - | & .st-live[data-state="live"] { color: var(--bar-ink); } | 50 | + | display: flex; align-items: center; gap: var(--s2); flex: none; |
| 57 | - | & .st-live[data-state="live"]::before { content: "● "; } | 51 | + | font: 700 var(--fs) / 1 var(--mono); color: var(--ink-hi); text-decoration: none; |
| 58 | - | & .st-owner { flex: none; display: flex; gap: 1ch; } | 52 | + | & span { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| 59 | - | & .st-owner span { color: var(--bar-ink); } | 53 | + | &:hover { text-decoration: none; } |
| 60 | - | & .st-owner button { color: var(--bar-dim); } | 54 | + | } |
| 61 | - | & .st-owner button:hover { color: #fff; text-decoration: underline; } | 55 | + | .brand-mark { |
| 62 | - | & .st-pos { min-width: 7ch; text-align: right; color: var(--bar-ink); } | 56 | + | position: relative; width: 28px; height: 28px; flex: none; |
| 57 | + | background: var(--ink-hi); border-radius: var(--r-s); | |||
| 58 | + | &::after { content: ""; position: absolute; inset: 6px; background: var(--on-ink); mask: url(icons/brand.svg) center / contain no-repeat; } | |||
| 59 | + | } | |||
| 60 | + | .crumbs { | |||
| 61 | + | min-width: 0; flex: 1 1 auto; overflow: hidden; | |||
| 62 | + | & ol { display: flex; align-items: center; min-width: 0; font-family: var(--mono); font-size: var(--fs); } | |||
| 63 | + | & li { display: flex; align-items: center; min-width: 0; white-space: nowrap; } | |||
| 64 | + | & li:last-child { overflow: hidden; } | |||
| 65 | + | & li::before { content: "/"; padding: 0 var(--s2); color: var(--border-strong); } | |||
| 66 | + | & a, & span { overflow: hidden; text-overflow: ellipsis; color: var(--ink-dim); text-decoration: none; } | |||
| 67 | + | & a:hover { color: var(--ink-hi); text-decoration: underline; } | |||
| 68 | + | & [aria-current] { color: var(--ink-hi); font-weight: 700; } | |||
| 69 | + | } | |||
| 70 | + | .hdr-acts { display: flex; align-items: center; gap: var(--s2); margin-left: auto; flex: none; } | |||
| 71 | + | .ref { | |||
| 72 | + | font-family: var(--mono); max-width: 28ch; | |||
| 73 | + | & .ref-name { overflow: hidden; text-overflow: ellipsis; } | |||
| 74 | + | & .sha { font-weight: 400; color: var(--ink-mute); } | |||
| 75 | + | } | |||
| 76 | + | .search { | |||
| 77 | + | min-width: 220px; justify-content: flex-start; color: var(--ink-mute); font-weight: 400; | |||
| 78 | + | background: var(--bg-inset); | |||
| 79 | + | & .search-label { flex: 1; } | |||
| 80 | + | } | |||
| 81 | + | .owner { | |||
| 82 | + | display: flex; align-items: center; gap: var(--s2); | |||
| 83 | + | & .owner-badge { font: 600 var(--fs-xs) / 1 var(--mono); color: var(--ink-dim); padding: 0 var(--s1); } | |||
| 63 | } | 84 | } | ||
| 64 | 85 | ||||
| 65 | - | /* work area: content + context rail. | 86 | + | .tabs { |
| 66 | - | Tables and code take the free width and the rail is fixed; on prose pages the text | 87 | + | max-width: var(--page); margin: 0 auto; padding: 0 var(--pad); |
| 67 | - | is capped at the reading measure and the rail takes the rest, so no column is idle. */ | 88 | + | overflow-x: auto; scrollbar-width: none; |
| 68 | - | .work { min-width: 0; } | 89 | + | & ul { display: flex; gap: var(--s1); } |
| 69 | - | .rail { padding: 0 var(--pad) var(--lh); min-width: 0; } | 90 | + | & a { |
| 70 | - | @media (min-width: 1100px) { | 91 | + | position: relative; display: flex; align-items: center; gap: var(--s2); height: 44px; padding: 0 var(--s3); |
| 71 | - | /* app shell: the bars stay put, the content and the rail scroll on their own, so each | 92 | + | color: var(--ink-dim); text-decoration: none; white-space: nowrap; font-weight: 600; |
| 72 | - | column has its scrollbar at its own edge (the README's sits next to the README) */ | |||
| 73 | - | body { height: 100dvh; overflow: hidden; } | |||
| 74 | - | .work { display: grid; grid-template-columns: minmax(0, 1fr); min-height: 0; } | |||
| 75 | - | .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); } | |||
| 76 | - | .work.has-rail.read { grid-template-columns: minmax(0, calc(var(--measure) + 2 * var(--pad))) minmax(var(--rail), 1fr); } | |||
| 77 | - | main, .rail { min-height: 0; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--rule) transparent; } | |||
| 78 | - | main:focus { outline: none; } | |||
| 79 | - | .rail { border-left: 1px solid var(--rule); } | |||
| 80 | - | :root { --stick: 0px; } | |||
| 81 | - | } | |||
| 82 | - | .rail-d > summary { display: none; } | |||
| 83 | - | @media (max-width: 1099.98px) { | |||
| 84 | - | /* the rail folds into one line above the content */ | |||
| 85 | - | .work.has-rail { display: flex; flex-direction: column; } | |||
| 86 | - | .rail { order: -1; border-bottom: 1px solid var(--rule); padding-bottom: 0; } | |||
| 87 | - | .rail-d > summary { | |||
| 88 | - | display: block; list-style: none; color: var(--ink-dim); | |||
| 89 | - | overflow: hidden; white-space: nowrap; text-overflow: ellipsis; | |||
| 90 | } | 93 | } | ||
| 91 | - | .rail-d > summary::-webkit-details-marker { display: none; } | 94 | + | & a::after { content: ""; position: absolute; left: var(--s2); right: var(--s2); bottom: -1px; height: 2px; border-radius: 2px; } |
| 92 | - | .rail-d > summary::before { content: "▸ "; color: var(--ink); } | 95 | + | & a:hover { color: var(--ink-hi); } |
| 93 | - | .rail-d[open] > summary::before { content: "▾ "; } | 96 | + | & a:hover::after { background: var(--border-strong); } |
| 94 | - | .rail-d[open] { padding-bottom: var(--lh); } | 97 | + | & a[aria-current] { color: var(--ink-hi); } |
| 95 | - | } | 98 | + | & a[aria-current]::after { background: var(--ink-hi); } |
| 96 | - | @media (max-width: 700px) { | 99 | + | & kbd { opacity: .55; } |
| 97 | - | /* the sections drop to a second bar row instead of being cut off */ | 100 | + | & a:hover kbd, & a:focus-visible kbd, & a[aria-current] kbd { opacity: 1; } |
| 98 | - | .top { flex-wrap: wrap; height: auto; } | |||
| 99 | - | .top .secs { order: 10; flex-basis: 100%; margin: 0; border-top: 1px solid #202020; } | |||
| 100 | - | .top .crumbs { flex: 1; min-width: 0; overflow: hidden; } | |||
| 101 | - | .status .st-hints, .status .st-ctx { display: none; } | |||
| 102 | - | .list .c-who, .list .c-desc, .list .c-sub, .list .c-ref, .list .c-size, .list .c-lang { display: none; } | |||
| 103 | } | 101 | } | ||
| 104 | 102 | ||||
| 105 | - | /* rail blocks: a dim title rule, then key/value rows on the character grid */ | 103 | + | /* work area: content and rail, centred, the page scrolls */ |
| 106 | - | .rail section { margin-top: var(--lh); } | 104 | + | .work { |
| 107 | - | .rail h2 { | 105 | + | flex: 1; width: 100%; max-width: var(--page); margin: 0 auto; |
| 108 | - | display: flex; gap: 1ch; color: var(--ink-dim); font-weight: normal; | 106 | + | padding: var(--s5) var(--pad) var(--s7); |
| 109 | - | &::after { content: ""; flex: 1; align-self: center; border-top: 1px solid var(--rule); } | 107 | + | display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s5); align-items: start; |
| 110 | } | 108 | } | ||
| 109 | + | .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); } | |||
| 110 | + | .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; } | |||
| 111 | + | main > * + * { margin-top: var(--s4); } | |||
| 112 | + | ||||
| 113 | + | /* blocks: a surface with a header; rail sections are blocks too */ | |||
| 114 | + | .card, .rail section { | |||
| 115 | + | background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); | |||
| 116 | + | min-width: 0; | |||
| 117 | + | } | |||
| 118 | + | .card-h, .rail h2 { | |||
| 119 | + | display: flex; align-items: center; gap: var(--s2); | |||
| 120 | + | padding: var(--s3) var(--s4); border-bottom: 1px solid var(--border); | |||
| 121 | + | font-size: var(--fs); line-height: 20px; | |||
| 122 | + | & a { margin-left: auto; font: 600 var(--fs-s) / 1 var(--font); color: var(--ink-dim); text-decoration: none; } | |||
| 123 | + | & a:hover { color: var(--ink-hi); text-decoration: underline; } | |||
| 124 | + | } | |||
| 125 | + | .rail section > :not(h2) { margin: var(--s3) var(--s4); } | |||
| 126 | + | .rail section > .list { margin: var(--s2) 0; } | |||
| 127 | + | .rail section > p + p { margin-top: calc(-1 * var(--s2)); } | |||
| 128 | + | ||||
| 111 | .kv { | 129 | .kv { | ||
| 112 | - | display: grid; grid-template-columns: 11ch minmax(0, 1fr); column-gap: 1ch; | 130 | + | display: grid; grid-template-columns: max-content minmax(0, 1fr); column-gap: var(--s4); row-gap: var(--s1); |
| 131 | + | font-size: var(--fs-s); | |||
| 113 | & dt { color: var(--ink-mute); } | 132 | & dt { color: var(--ink-mute); } | ||
| 114 | - | & dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | 133 | + | & dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); } |
| 115 | } | 134 | } | ||
| 116 | .outline { | 135 | .outline { | ||
| 117 | - | & a { display: block; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | 136 | + | font-size: var(--fs-s); |
| 118 | - | & a:hover { text-decoration: underline; } | 137 | + | & a { display: block; padding: 2px var(--s2); margin: 0 calc(-1 * var(--s2)); border-radius: 4px; color: var(--ink-dim); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| 119 | - | & .l1 { font-weight: bold; } | 138 | + | & a:hover { background: var(--bg-raised); color: var(--ink-hi); } |
| 120 | - | & .l3 { padding-left: 2ch; } | 139 | + | & .l1 { font-weight: 700; color: var(--ink); } |
| 121 | - | & .l4, & .l5, & .l6 { padding-left: 4ch; } | 140 | + | & .l3 { padding-left: var(--s5); } |
| 122 | - | & a[aria-current]::before { content: "▸"; margin-left: -1ch; } | 141 | + | & .l4, & .l5, & .l6 { padding-left: var(--s6); } |
| 123 | - | } | 142 | + | } |
| 124 | - | .rail .clone { border: 0; padding: 0; margin-top: 0; } | 143 | + | |
| 125 | - | .rail .clone-line { flex-wrap: wrap; } | 144 | + | /* footer: version and source (AGPL §13) */ |
| 145 | + | .foot { | |||
| 146 | + | display: flex; flex-wrap: wrap; gap: var(--s4); align-items: center; | |||
| 147 | + | width: 100%; max-width: var(--page); margin: 0 auto; padding: var(--s4) var(--pad) var(--s5); | |||
| 148 | + | font-size: var(--fs-s); color: var(--ink-mute); | |||
| 149 | + | &::before { content: ""; flex-basis: 100%; height: 1px; margin-bottom: var(--s2); background: var(--border); } | |||
| 150 | + | & a { color: var(--ink-dim); } | |||
| 151 | + | & .foot-keys { margin-left: auto; } | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | /* hint: bottom right, only while it has something to say */ | |||
| 155 | + | .hint { | |||
| 156 | + | position: fixed; right: var(--s4); bottom: var(--s4); z-index: 25; | |||
| 157 | + | display: flex; gap: var(--s3); align-items: center; | |||
| 158 | + | padding: var(--s2) var(--s3); font: 600 var(--fs-s) / 18px var(--mono); color: var(--ink-hi); | |||
| 159 | + | background: var(--bg-overlay); border: 1px solid var(--border-strong); border-radius: var(--r); box-shadow: var(--shadow); | |||
| 160 | + | pointer-events: none; | |||
| 161 | + | & > :empty { display: none; } | |||
| 162 | + | & .hint-live { color: var(--red); } | |||
| 163 | + | } | |||
| 164 | + | .hint:not(:has(> :not(:empty))) { display: none; } | |||
| 165 | + | ||||
| 166 | + | @media (max-width: 1080px) { | |||
| 167 | + | /* the rail goes under the content, in two columns while there is room */ | |||
| 168 | + | .work.has-rail { grid-template-columns: minmax(0, 1fr); } | |||
| 169 | + | .rail { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); align-items: start; } | |||
| 170 | + | .search { min-width: 0; } | |||
| 171 | + | .search .search-label, .search kbd { display: none; } | |||
| 172 | + | .search { width: 32px; padding: 0; justify-content: center; } | |||
| 173 | + | } | |||
| 174 | + | @media (max-width: 720px) { | |||
| 175 | + | :root { --pad: var(--s4); } | |||
| 176 | + | .hdr-top { gap: var(--s3); height: 52px; } | |||
| 177 | + | .brand span { display: none; } | |||
| 178 | + | .ref .sha, .owner-badge, .hdr-acts .icon { display: none; } | |||
| 179 | + | .ref { max-width: 16ch; } | |||
| 180 | + | .work { padding-top: var(--s4); } | |||
| 181 | + | .rail { grid-template-columns: minmax(0, 1fr); } | |||
| 182 | + | .tabs kbd { display: none; } | |||
| 183 | + | .foot .foot-keys { display: none; } | |||
| 184 | + | .list .c-who, .list .c-desc, .list .c-sub, .list .c-ref, .list .c-size, .list .c-lang { display: none; } | |||
| 185 | + | } | |||
| 126 | } | 186 | } | ||
M internal/adapters/web/static/css/25-panel.css +58 -33
| @@ -1,41 +1,66 @@ | |||||
| 1 | @layer components { | 1 | @layer components { | ||
| 2 | - | /* bottom panels above the status bar (help, ref switcher, ":" command line) */ | 2 | + | /* dialogs: the "go to" palette, the ref switcher and the keys, centred over a backdrop */ |
| 3 | - | .panel { | 3 | + | .modal { |
| 4 | - | position: fixed; inset: auto 0 var(--bar-h) 0; margin: 0; width: 100%; z-index: 15; | 4 | + | position: fixed; inset: 0; margin: 12vh auto auto; z-index: 40; |
| 5 | - | background: var(--paper); color: var(--ink); | 5 | + | width: min(640px, calc(100vw - 2 * var(--s4))); max-height: min(70dvh, 560px); |
| 6 | - | border-top: 2px solid var(--ink); | 6 | + | display: none; flex-direction: column; |
| 7 | - | max-height: min(46dvh, 28rem); overflow: auto; overscroll-behavior: contain; | 7 | + | background: var(--bg-overlay); color: var(--ink); |
| 8 | - | padding: 0 var(--pad) var(--lh); | 8 | + | border: 1px solid var(--border-strong); border-radius: var(--r); box-shadow: var(--shadow); |
| 9 | - | &:not([open]) { display: none; } | 9 | + | overflow: hidden; overscroll-behavior: contain; |
| 10 | - | & .panel-head { | 10 | + | &[open] { display: flex; } |
| 11 | - | position: sticky; top: 0; background: var(--paper); display: flex; gap: 1ch; | 11 | + | &::backdrop { background: var(--backdrop); } |
| 12 | - | align-items: baseline; height: calc(var(--lh) + 6px); padding-top: 4px; | 12 | + | &#panel-help { width: min(820px, calc(100vw - 2 * var(--s4))); } |
| 13 | - | border-bottom: 1px solid var(--rule); margin-bottom: 4px; | 13 | + | } |
| 14 | - | & input { flex: 1; min-width: 0; outline-offset: 2px; } | 14 | + | @media (prefers-reduced-motion: no-preference) { |
| 15 | - | & button { margin-left: auto; color: var(--ink-dim); white-space: nowrap; } | 15 | + | .modal[open] { animation: modal-in 120ms ease-out; } |
| 16 | - | & button:hover { color: var(--ink); text-decoration: underline; } | 16 | + | @keyframes modal-in { from { opacity: 0; transform: translateY(-6px) scale(.99); } } |
| 17 | - | & .hi { font-weight: bold; } | 17 | + | } |
| 18 | + | .panel-head { | |||
| 19 | + | display: flex; align-items: center; gap: var(--s3); flex: none; | |||
| 20 | + | padding: var(--s3) var(--s4); border-bottom: 1px solid var(--border); | |||
| 21 | + | & h2 { font-size: var(--fs); flex: 1; } | |||
| 22 | + | & input { flex: 1; min-width: 0; height: 32px; font-size: 16px; } | |||
| 23 | + | & input::placeholder { color: var(--ink-mute); opacity: 1; } | |||
| 24 | + | & input:focus-visible { outline: none; } | |||
| 25 | + | } | |||
| 26 | + | .panel-x { color: var(--ink-mute); font-size: var(--fs-s); display: flex; gap: var(--s2); align-items: center; } | |||
| 27 | + | .panel-x:hover { color: var(--ink-hi); } | |||
| 28 | + | .panel-foot { | |||
| 29 | + | display: flex; gap: var(--s4); flex: none; padding: var(--s2) var(--s4); | |||
| 30 | + | border-top: 1px solid var(--border); background: var(--bg-surface); | |||
| 31 | + | font-size: var(--fs-xs); color: var(--ink-mute); | |||
| 32 | + | & span { display: flex; gap: var(--s1); align-items: center; } | |||
| 33 | + | } | |||
| 34 | + | .modal > ul, .modal > .panel-cols, .modal > .keys { overflow-y: auto; padding: var(--s2); } | |||
| 35 | + | ||||
| 36 | + | .panel-cols { | |||
| 37 | + | display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--s2); | |||
| 38 | + | & h2 { font: 600 var(--fs-xs) / 1 var(--font); text-transform: uppercase; letter-spacing: .06em; color: var(--ink-mute); padding: var(--s3) var(--s3) var(--s2); } | |||
| 39 | + | & li a { | |||
| 40 | + | display: flex; align-items: center; gap: var(--s2); padding: 6px var(--s3); border-radius: var(--r-s); | |||
| 41 | + | font-family: var(--mono); font-size: var(--fs-s); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; | |||
| 18 | } | 42 | } | ||
| 19 | - | & .panel-cols { display: grid; grid-template-columns: repeat(auto-fill, minmax(34ch, 1fr)); column-gap: 3ch; } | 43 | + | & li a:hover, & li a:focus-visible { background: var(--bg-raised); color: var(--ink-hi); } |
| 20 | - | & .panel-cols h2 { color: var(--ink-dim); font-weight: normal; } | 44 | + | & li a[aria-current] { font-weight: 700; color: var(--ink-hi); } |
| 21 | - | & li a { display: block; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | 45 | + | & .mark { display: inline-block; width: 1ch; color: var(--teal); } |
| 22 | - | & li a:hover, & li a:focus-visible { background: var(--sel); } | |||
| 23 | - | & li a[aria-current] { font-weight: bold; } | |||
| 24 | - | & .mark { display: inline-block; width: 2ch; } | |||
| 25 | } | 46 | } | ||
| 47 | + | ||||
| 26 | .cmd-list { | 48 | .cmd-list { | ||
| 27 | - | columns: 34ch; column-gap: 3ch; | 49 | + | & li a { |
| 28 | - | & li { break-inside: avoid; } | 50 | + | display: flex; align-items: center; gap: var(--s3); padding: var(--s2) var(--s3); border-radius: var(--r-s); |
| 29 | - | & li[data-cursor] a { background: var(--row); } | 51 | + | text-decoration: none; color: var(--ink); |
| 30 | - | & li[data-cursor] a::before { content: "▸"; position: absolute; margin-left: -1.5ch; } | 52 | + | } |
| 31 | - | & a { padding-left: 1.5ch; } | 53 | + | & li a:hover { background: var(--bg-raised); } |
| 32 | - | & .k { display: inline-block; width: 8ch; color: var(--ink-mute); } | 54 | + | & li[data-cursor] a { background: var(--bg-strong); color: var(--ink-hi); } |
| 55 | + | & .lbl { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: var(--fs-s); } | |||
| 56 | + | & .k { flex: none; font-size: var(--fs-xs); color: var(--ink-mute); } | |||
| 33 | } | 57 | } | ||
| 58 | + | ||||
| 34 | .keys { | 59 | .keys { | ||
| 35 | - | display: grid; grid-template-columns: repeat(auto-fill, minmax(46ch, 1fr)); column-gap: 3ch; | 60 | + | display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); column-gap: var(--s5); row-gap: var(--s1); |
| 36 | - | & > div { display: grid; grid-template-columns: 14ch 1fr; } | 61 | + | padding: var(--s3) var(--s4) var(--s4) !important; |
| 37 | - | & dt { white-space: nowrap; } | 62 | + | & > div { display: grid; grid-template-columns: 130px 1fr; gap: var(--s3); align-items: baseline; padding: var(--s1) 0; border-bottom: 1px solid var(--border); } |
| 38 | - | & dd { color: var(--ink-dim); } | 63 | + | & dt { white-space: nowrap; display: flex; gap: var(--s1); } |
| 39 | - | & kbd { border: 1px solid var(--rule); padding: 0 .6ch; } | 64 | + | & dd { color: var(--ink-dim); font-size: var(--fs-s); } |
| 40 | } | 65 | } | ||
| 41 | } | 66 | } | ||
M internal/adapters/web/static/css/30-list.css +76 -45
| @@ -1,76 +1,107 @@ | |||||
| 1 | @layer components { | 1 | @layer components { | ||
| 2 | - | /* plain-text tables with a ▸ row cursor */ | 2 | + | /* tables in a card, with a row cursor (j/k) */ |
| 3 | .list { | 3 | .list { | ||
| 4 | width: 100%; table-layout: fixed; | 4 | width: 100%; table-layout: fixed; | ||
| 5 | - | & th { color: var(--ink-mute); font-weight: normal; white-space: nowrap; border-bottom: 1px solid var(--rule); } | 5 | + | background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); |
| 6 | + | border-collapse: separate; overflow: hidden; | |||
| 7 | + | & th { | |||
| 8 | + | height: 40px; padding: 0 var(--s3); white-space: nowrap; | |||
| 9 | + | font: 600 var(--fs-xs) / 1 var(--font); text-transform: uppercase; letter-spacing: .05em; color: var(--ink-mute); | |||
| 10 | + | background: var(--bg-raised); border-bottom: 1px solid var(--border); | |||
| 11 | + | } | |||
| 6 | & th a { text-decoration: none; } | 12 | & th a { text-decoration: none; } | ||
| 7 | - | & th a:hover { text-decoration: underline; color: var(--ink); } | 13 | + | & th a:hover { color: var(--ink-hi); } |
| 8 | - | & th[aria-sort="ascending"], & th[aria-sort="descending"] { color: var(--ink); font-weight: bold; } | 14 | + | & th[aria-sort="ascending"], & th[aria-sort="descending"] { color: var(--ink-hi); } |
| 9 | - | & td, & th { padding-right: 2ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | 15 | + | & td { height: 40px; padding: 0 var(--s3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-top: 1px solid var(--border); } |
| 10 | - | & .cur { width: 2ch; padding: 0; } | 16 | + | & tbody tr:first-child td { border-top: 0; } |
| 11 | - | & tbody tr[data-cursor] { background: var(--row); } | 17 | + | & th.cur, & td.cur { width: 3px; padding: 0; } |
| 12 | - | & tbody tr[data-cursor] .cur::before { content: "▸"; font-weight: bold; } | 18 | + | & tbody tr:hover { background: var(--bg-raised); } |
| 13 | - | & tbody tr:hover { background: var(--row); } | 19 | + | & tbody tr[data-cursor] { background: var(--bg-raised); } |
| 20 | + | & tbody tr[data-cursor] .cur { background: var(--ink-hi); } | |||
| 14 | & tbody tr[hidden] { display: none; } | 21 | & tbody tr[hidden] { display: none; } | ||
| 15 | & td > a { text-decoration: none; } | 22 | & td > a { text-decoration: none; } | ||
| 23 | + | & td > a:first-child { color: var(--ink-hi); } | |||
| 16 | & tr:hover td > a:first-child, & tr[data-cursor] td > a:first-child { text-decoration: underline; } | 24 | & tr:hover td > a:first-child, & tr[data-cursor] td > a:first-child { text-decoration: underline; } | ||
| 25 | + | & .c-sha, & .c-ref, & .c-kind, & .c-size, & .c-n { font-family: var(--mono); font-size: var(--fs-s); } | |||
| 26 | + | & .c-sha a { color: var(--ink-dim); } | |||
| 27 | + | & .c-when, & .c-who { font-size: var(--fs-s); } | |||
| 17 | & .c-sub { color: var(--ink-dim); } | 28 | & .c-sub { color: var(--ink-dim); } | ||
| 18 | - | & .c-sub .refs { color: var(--ink); } | |||
| 19 | } | 29 | } | ||
| 20 | 30 | ||||
| 21 | .home { | 31 | .home { | ||
| 22 | - | & .c-name { width: 24ch; font-weight: bold; } | 32 | + | & .c-name { width: 26%; font-family: var(--mono); font-weight: 700; } |
| 23 | - | & th.c-name { font-weight: normal; } | 33 | + | & th.c-name { font-family: var(--font); } |
| 24 | - | & th.c-name[aria-sort] { font-weight: bold; } | |||
| 25 | & .c-desc { width: auto; } | 34 | & .c-desc { width: auto; } | ||
| 26 | - | & .c-when { width: 13ch; } | 35 | + | & .c-when { width: 130px; } |
| 27 | - | & .c-ref { width: 10ch; } | 36 | + | & .c-ref { width: 120px; } |
| 28 | - | & .c-lang { width: 12ch; padding-right: 0; } | 37 | + | & .c-lang { width: 120px; } |
| 29 | } | 38 | } | ||
| 30 | .list.tree { | 39 | .list.tree { | ||
| 31 | - | & .c-kind { width: 2ch; color: var(--ink-mute); padding-right: 0; } | 40 | + | & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; } |
| 32 | - | & .c-name { width: 34ch; } | 41 | + | & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); } |
| 33 | - | & .c-size { width: 9ch; text-align: right; padding-right: 3ch; } | 42 | + | & .c-size { width: 90px; text-align: right; } |
| 34 | - | & .c-when { width: 11ch; } | 43 | + | & .c-when { width: 100px; } |
| 35 | & .c-sub { width: auto; } | 44 | & .c-sub { width: auto; } | ||
| 36 | - | & .c-who { width: 12ch; } | 45 | + | & .c-who { width: 130px; } |
| 37 | } | 46 | } | ||
| 38 | .list.commits, .list.refs { | 47 | .list.commits, .list.refs { | ||
| 39 | - | & .c-sha { width: 8ch; } | 48 | + | & .c-sha { width: 96px; } |
| 40 | - | & .c-who { width: 12ch; } | 49 | + | & .c-who { width: 140px; } |
| 41 | - | & .c-when { width: 11ch; } | 50 | + | & .c-when { width: 100px; } |
| 42 | } | 51 | } | ||
| 43 | 52 | ||||
| 44 | - | /* filter line (home, tree, commits) */ | 53 | + | /* filter: a search field above a list */ |
| 45 | .filter { | 54 | .filter { | ||
| 46 | - | display: flex; gap: 1ch; height: var(--lh); margin: var(--lh) 0 0; | 55 | + | position: relative; display: flex; max-width: 420px; |
| 47 | - | & label { color: var(--ink-mute); } | 56 | + | &::before { |
| 48 | - | & input { flex: 1; min-width: 0; } | 57 | + | content: ""; position: absolute; left: 11px; top: 10px; width: 16px; height: 16px; pointer-events: none; |
| 58 | + | background: var(--ink-mute); mask: url(icons/search.svg) center / contain no-repeat; | |||
| 59 | + | } | |||
| 60 | + | &::after { | |||
| 61 | + | content: "/"; position: absolute; right: 8px; top: 50%; transform: translateY(-50%); | |||
| 62 | + | font: 600 var(--fs-xs) / 18px var(--mono); color: var(--ink-mute); padding: 0 6px; | |||
| 63 | + | border: 1px solid var(--border-strong); border-bottom-width: 2px; border-radius: 4px; pointer-events: none; | |||
| 64 | + | } | |||
| 65 | + | & input { | |||
| 66 | + | flex: 1; min-width: 0; height: 36px; padding: 0 36px 0 34px; | |||
| 67 | + | background: var(--bg-surface); border: 1px solid var(--border-strong); border-radius: var(--r-s); | |||
| 68 | + | } | |||
| 69 | + | & input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; border-color: transparent; } | |||
| 49 | & input::placeholder { color: var(--ink-mute); opacity: 1; } | 70 | & input::placeholder { color: var(--ink-mute); opacity: 1; } | ||
| 71 | + | &:focus-within::after { display: none; } | |||
| 50 | } | 72 | } | ||
| 51 | 73 | ||||
| 52 | - | /* headings inside pages read as pager section rules */ | 74 | + | /* section headings inside a page */ |
| 53 | .rule-h { | 75 | .rule-h { | ||
| 54 | - | display: flex; gap: 1ch; margin: var(--lh) 0 0; color: var(--ink-dim); | 76 | + | display: flex; align-items: center; gap: var(--s2); margin-top: var(--s5); |
| 55 | - | &::after { content: ""; flex: 1; align-self: center; border-top: 1px solid var(--rule); } | 77 | + | font-size: var(--fs); |
| 56 | - | & > :first-child { font-weight: bold; color: var(--ink); } | 78 | + | & > :first-child { color: var(--ink-hi); } |
| 57 | } | 79 | } | ||
| 80 | + | main > .rule-h:first-child { margin-top: 0; } | |||
| 58 | 81 | ||||
| 59 | - | /* empty / 404 / error */ | 82 | + | /* empty / 404 / error / sign in: one card in the middle */ |
| 60 | .state { | 83 | .state { | ||
| 61 | - | margin-top: var(--lh); max-width: var(--measure); | 84 | + | max-width: 560px; margin: var(--s7) auto 0 !important; padding: var(--s6); |
| 62 | - | & h1 { margin-bottom: var(--lh); } | 85 | + | background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); |
| 63 | - | & .code { display: inline-block; min-width: 6ch; } | 86 | + | & .eyebrow { font: 700 var(--fs-s) / 1 var(--mono); color: var(--ink-mute); text-transform: uppercase; letter-spacing: .08em; margin-bottom: var(--s2); } |
| 64 | - | & .state-back { margin-top: var(--lh); display: flex; gap: 2ch; } | 87 | + | & h1 { margin-bottom: var(--s3); } |
| 65 | - | & .state-act { margin-top: var(--lh); } | 88 | + | & p.dim { color: var(--ink-dim); } |
| 66 | - | & .state-act button { color: var(--ink-hi); font-weight: bold; } | 89 | + | & .state-act { margin-top: var(--s5); } |
| 67 | - | & .state-act button:hover { text-decoration: underline; } | 90 | + | & .state-back { margin-top: var(--s5); display: flex; gap: var(--s4); font-size: var(--fs-s); } |
| 91 | + | & .clone { margin-top: var(--s4); } | |||
| 68 | } | 92 | } | ||
| 93 | + | ||||
| 94 | + | /* copyable shell lines */ | |||
| 69 | .clone { | 95 | .clone { | ||
| 70 | - | margin: var(--lh) 0 0; border: 1px solid var(--rule); padding: 0 1ch; | 96 | + | display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s2); min-width: 0; |
| 71 | - | & .clone-line { display: flex; gap: 1ch; align-items: baseline; } | 97 | + | & .clone-line { |
| 72 | - | & code { flex: 1; min-width: 0; overflow-x: auto; white-space: pre; } | 98 | + | min-width: 0; |
| 73 | - | & .copy { color: var(--ink-dim); white-space: nowrap; min-width: 9ch; text-align: right; } | 99 | + | display: flex; gap: var(--s2); align-items: center; padding: var(--s1) var(--s1) var(--s1) var(--s3); |
| 74 | - | & .copy:hover { color: var(--ink); text-decoration: underline; } | 100 | + | background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--r-s); |
| 101 | + | font-size: var(--fs-code); | |||
| 102 | + | } | |||
| 103 | + | & .clone-line > .mute { user-select: none; font-family: var(--mono); } | |||
| 104 | + | & code { flex: 1; min-width: 0; overflow-x: auto; white-space: pre; scrollbar-width: none; } | |||
| 105 | + | & .copy { height: 26px; padding: 0 var(--s2); font-size: var(--fs-xs); } | |||
| 75 | } | 106 | } | ||
| 76 | } | 107 | } | ||
M internal/adapters/web/static/css/40-summary.css +34 -23
| @@ -1,38 +1,49 @@ | |||||
| 1 | @layer components { | 1 | @layer components { | ||
| 2 | - | .lede { margin-top: var(--lh); max-width: var(--measure); } | 2 | + | .lede { max-width: var(--measure); color: var(--ink-dim); font-size: 16px; } |
| 3 | 3 | ||||
| 4 | - | /* key/value facts, aligned on a 12ch key column */ | 4 | + | /* key/value facts */ |
| 5 | .facts { | 5 | .facts { | ||
| 6 | - | margin-top: var(--lh); display: grid; grid-template-columns: repeat(auto-fill, minmax(38ch, 1fr)); column-gap: 3ch; | 6 | + | display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); column-gap: var(--s5); |
| 7 | - | & > div { display: grid; grid-template-columns: 12ch 1fr; min-width: 0; } | 7 | + | & > div { display: grid; grid-template-columns: 120px 1fr; min-width: 0; } |
| 8 | & dt { color: var(--ink-mute); } | 8 | & dt { color: var(--ink-mute); } | ||
| 9 | & dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | 9 | & dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | ||
| 10 | } | 10 | } | ||
| 11 | 11 | ||||
| 12 | - | .tagged { color: var(--ink); font-weight: bold; white-space: nowrap; } | 12 | + | .tagged { color: var(--ink); font-weight: 700; white-space: nowrap; } |
| 13 | .list.commits .c-subject { overflow: hidden; text-overflow: ellipsis; } | 13 | .list.commits .c-subject { overflow: hidden; text-overflow: ellipsis; } | ||
| 14 | 14 | ||||
| 15 | - | /* markdown, rendered from the fixture */ | 15 | + | .readme .prose { padding: var(--s5) var(--s6) var(--s6); } |
| 16 | + | ||||
| 17 | + | /* rendered Markdown */ | |||
| 16 | .prose { | 18 | .prose { | ||
| 17 | - | max-width: var(--measure); margin-top: var(--lh); | 19 | + | max-width: var(--measure); line-height: 1.65; overflow-wrap: break-word; |
| 18 | - | & h1, & h2, & h3, & h4 { margin: var(--lh) 0 0; } | 20 | + | & h1, & h2, & h3, & h4 { margin: 1.6em 0 0; } |
| 19 | - | & h1::before { content: "# "; color: var(--ink-mute); } | 21 | + | & h1 { font-size: 26px; line-height: 34px; padding-bottom: var(--s2); border-bottom: 1px solid var(--border); } |
| 20 | - | & h2::before { content: "## "; color: var(--ink-mute); } | 22 | + | & h2 { font-size: 20px; line-height: 28px; padding-bottom: var(--s1); border-bottom: 1px solid var(--border); } |
| 21 | - | & h3::before { content: "### "; color: var(--ink-mute); } | 23 | + | & h3 { font-size: 17px; } |
| 22 | - | & h4::before { content: "#### "; color: var(--ink-mute); } | |||
| 23 | & > :first-child { margin-top: 0; } | 24 | & > :first-child { margin-top: 0; } | ||
| 24 | - | & p, & ul, & ol, & pre, & table, & blockquote { margin-top: var(--lh); } | 25 | + | & p, & ul, & ol, & pre, & table, & blockquote { margin-top: var(--s4); } |
| 25 | - | & ul { list-style: none; & > li::before { content: "- "; color: var(--ink-mute); margin-left: -2ch; } } | 26 | + | & ul { list-style: disc; } |
| 26 | - | & ul, & ol { padding-left: 2ch; } | 27 | + | & ul, & ol { padding-left: var(--s5); } |
| 27 | & ol { list-style: decimal; } | 28 | & ol { list-style: decimal; } | ||
| 28 | - | & li > ul, & li > ol, & li > p { margin-top: 0; } | 29 | + | & li + li { margin-top: var(--s1); } |
| 29 | - | & pre { padding: 0 1ch; border-left: 1px solid var(--ink-mute); overflow-x: auto; font-size: var(--fs-code); line-height: var(--lh-code); } | 30 | + | & li::marker { color: var(--ink-mute); } |
| 30 | - | & :not(pre) > code { border-bottom: 1px dotted var(--ink-mute); overflow-wrap: anywhere; } | 31 | + | & li > ul, & li > ol, & li > p { margin-top: var(--s1); } |
| 31 | - | & blockquote { padding-left: 2ch; border-left: 1px solid var(--rule); color: var(--ink-dim); } | 32 | + | & pre { |
| 32 | - | & table { border-collapse: collapse; display: block; overflow-x: auto; } | 33 | + | padding: var(--s3) var(--s4); overflow-x: auto; |
| 33 | - | & th, & td { padding: 0 2ch 0 0; white-space: nowrap; vertical-align: top; } | 34 | + | background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--r-s); |
| 34 | - | & th { border-bottom: 1px solid var(--rule); } | 35 | + | font-size: var(--fs-code); line-height: var(--lh-code); |
| 35 | - | & hr { margin: var(--lh) 0; } | 36 | + | } |
| 37 | + | & :not(pre) > code { | |||
| 38 | + | padding: 1px 5px; font-size: .88em; overflow-wrap: anywhere; | |||
| 39 | + | background: var(--bg-raised); border: 1px solid var(--border); border-radius: 4px; | |||
| 40 | + | } | |||
| 41 | + | & blockquote { padding-left: var(--s4); border-left: 3px solid var(--border-strong); color: var(--ink-dim); } | |||
| 42 | + | & table { display: block; overflow-x: auto; border: 1px solid var(--border); border-radius: var(--r-s); font-size: var(--fs-s); line-height: 20px; } | |||
| 43 | + | & th, & td { padding: 6px var(--s3); white-space: nowrap; vertical-align: top; border-top: 1px solid var(--border); } | |||
| 44 | + | & th { background: var(--bg-raised); border-top: 0; font-weight: 700; } | |||
| 45 | + | & td code { white-space: nowrap; } | |||
| 46 | + | & hr { margin: var(--s5) 0; } | |||
| 36 | & .alt { color: var(--ink-mute); } | 47 | & .alt { color: var(--ink-mute); } | ||
| 37 | & img { display: none; } | 48 | & img { display: none; } | ||
| 38 | } | 49 | } | ||
M internal/adapters/web/static/css/50-code.css +48 -39
| @@ -1,17 +1,21 @@ | |||||
| 1 | @layer components { | 1 | @layer components { | ||
| 2 | /* page title for a file: name, then its actions as plain words */ | 2 | /* page title for a file: name, then its actions as plain words */ | ||
| 3 | .file-h { | 3 | .file-h { | ||
| 4 | - | display: flex; gap: 2ch; align-items: baseline; margin-top: var(--lh); | 4 | + | display: flex; gap: var(--s4); align-items: center; flex-wrap: wrap; font-size: 18px; |
| 5 | - | padding-bottom: 2px; border-bottom: 1px solid var(--rule); | 5 | + | & > span:first-child { overflow-wrap: anywhere; } |
| 6 | - | & .acts { margin-left: auto; font-weight: normal; display: flex; gap: 2ch; } | 6 | + | & .acts { margin-left: auto; display: flex; gap: var(--s2); } |
| 7 | } | 7 | } | ||
| 8 | - | .notice { margin-top: var(--lh); border-left: 2px solid var(--ink); padding-left: 1ch; max-width: var(--measure); } | 8 | + | .notice { |
| 9 | - | .note { color: var(--ink-dim); } | 9 | + | padding: var(--s3) var(--s4); max-width: var(--measure); font-size: var(--fs-s); |
| 10 | - | .pad { padding: 0 1ch; } | 10 | + | background: var(--bg-surface); border: 1px solid var(--border-strong); border-left: 3px solid var(--ink-hi); border-radius: var(--r-s); |
| 11 | + | } | |||
| 12 | + | .note { color: var(--ink-dim); font-size: var(--fs-s); } | |||
| 13 | + | .pad { padding: var(--s3) var(--s4); } | |||
| 11 | 14 | ||||
| 12 | /* source view: gutter numbers are links (selectable lines), lines span the full width */ | 15 | /* source view: gutter numbers are links (selectable lines), lines span the full width */ | ||
| 13 | .code { | 16 | .code { | ||
| 14 | - | margin: var(--lh) calc(var(--pad) * -1) 0; overflow-x: auto; | 17 | + | overflow-x: auto; padding: var(--s3) 0; font-family: var(--mono); |
| 18 | + | background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); | |||
| 15 | font-size: var(--fs-code); line-height: var(--lh-code); | 19 | font-size: var(--fs-code); line-height: var(--lh-code); | ||
| 16 | & code { display: grid; min-width: max-content; } | 20 | & code { display: grid; min-width: max-content; } | ||
| 17 | & .line { padding-right: var(--pad); border-left: 2px solid transparent; } | 21 | & .line { padding-right: var(--pad); border-left: 2px solid transparent; } | ||
| @@ -23,44 +27,43 @@ | |||||
| 23 | } | 27 | } | ||
| 24 | & .ln:hover, & .sel .ln { color: var(--ink-hi); } | 28 | & .ln:hover, & .sel .ln { color: var(--ink-hi); } | ||
| 25 | } | 29 | } | ||
| 26 | - | .source { margin-top: var(--lh); & > summary { color: var(--ink-dim); } } | 30 | + | .source > summary { color: var(--ink-dim); font-size: var(--fs-s); } |
| 31 | + | .source[open] > summary { margin-bottom: var(--s3); } | |||
| 32 | + | .img-view { padding: var(--s4); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); } | |||
| 27 | 33 | ||||
| 28 | /* commit header, like `git show` */ | 34 | /* commit header, like `git show` */ | ||
| 29 | .show { | 35 | .show { | ||
| 30 | - | margin-top: var(--lh); | 36 | + | padding: var(--s5); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); |
| 31 | - | & h1 { max-width: var(--measure); } | 37 | + | & h1 { max-width: var(--measure); overflow-wrap: anywhere; } |
| 32 | - | & .msg { margin-top: var(--lh); white-space: pre-wrap; max-width: var(--measure); color: var(--ink-dim); } | 38 | + | & .msg { margin-top: var(--s3); white-space: pre-wrap; max-width: var(--measure); color: var(--ink-dim); font-size: var(--fs-s); } |
| 33 | - | & .show-kv { margin-top: var(--lh); } | 39 | + | & .show-kv { margin-top: var(--s4); } |
| 34 | - | & .show-kv dd { white-space: normal; } | 40 | + | & .show-kv dd { white-space: normal; overflow-wrap: anywhere; } |
| 35 | } | 41 | } | ||
| 36 | .diff-tools { | 42 | .diff-tools { | ||
| 37 | - | display: flex; gap: 2ch; align-items: baseline; margin-top: var(--lh); | 43 | + | display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; |
| 38 | - | position: sticky; top: var(--stick); z-index: 5; background: var(--paper); | 44 | + | position: sticky; top: var(--hdr-h, 0px); z-index: 5; padding: var(--s2) 0; background: var(--bg-canvas); |
| 39 | - | border-bottom: 1px solid var(--rule); | |||
| 40 | & .grow { flex: 1; } | 45 | & .grow { flex: 1; } | ||
| 41 | - | & button { color: var(--ink-dim); } | |||
| 42 | - | & button:hover { color: var(--ink); text-decoration: underline; } | |||
| 43 | - | & button[aria-pressed="true"] { color: var(--ink-hi); font-weight: bold; } | |||
| 44 | - | & button[aria-pressed="true"]::before { content: "▸"; } | |||
| 45 | } | 46 | } | ||
| 46 | 47 | ||||
| 47 | /* one file of a diff */ | 48 | /* one file of a diff */ | ||
| 48 | .dfile { | 49 | .dfile { | ||
| 49 | - | margin-top: var(--lh); scroll-margin-top: calc(var(--stick) + var(--lh) + 4px); | 50 | + | margin-top: var(--s3); scroll-margin-top: calc(var(--hdr-h, 0px) + 56px); |
| 50 | & > summary { | 51 | & > summary { | ||
| 51 | - | position: sticky; top: calc(var(--stick) + var(--lh) + 1px); z-index: 4; | 52 | + | position: sticky; top: calc(var(--hdr-h, 0px) + 48px); z-index: 4; |
| 52 | - | background: var(--paper-alt); border: 1px solid var(--rule); padding: 0 1ch; | 53 | + | background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--r) var(--r) 0 0; padding: var(--s2) var(--s3); |
| 54 | + | font-family: var(--mono); font-size: var(--fs-s); | |||
| 53 | white-space: nowrap; overflow: hidden; text-overflow: ellipsis; list-style: none; | 55 | white-space: nowrap; overflow: hidden; text-overflow: ellipsis; list-style: none; | ||
| 54 | } | 56 | } | ||
| 57 | + | &:not([open]) > summary { border-radius: var(--r); } | |||
| 55 | & > summary::-webkit-details-marker { display: none; } | 58 | & > summary::-webkit-details-marker { display: none; } | ||
| 56 | & > summary::before { content: "▸ "; } | 59 | & > summary::before { content: "▸ "; } | ||
| 57 | &[open] > summary::before { content: "▾ "; } | 60 | &[open] > summary::before { content: "▾ "; } | ||
| 58 | & .st { font-weight: bold; } | 61 | & .st { font-weight: bold; } | ||
| 59 | & .p { font-weight: bold; } | 62 | & .p { font-weight: bold; } | ||
| 60 | - | & > .note { border: 1px solid var(--rule); border-top: 0; } | 63 | + | & > .note { border: 1px solid var(--border); border-top: 0; border-radius: 0 0 var(--r) var(--r); background: var(--bg-surface); } |
| 61 | } | 64 | } | ||
| 62 | .dt { | 65 | .dt { | ||
| 63 | - | width: max-content; min-width: 100%; | 66 | + | width: max-content; min-width: 100%; font-family: var(--mono); |
| 64 | font-size: var(--fs-code); line-height: var(--lh-code); | 67 | font-size: var(--fs-code); line-height: var(--lh-code); | ||
| 65 | & td { padding: 0; vertical-align: top; } | 68 | & td { padding: 0; vertical-align: top; } | ||
| 66 | & .ln { width: 6ch; min-width: 6ch; padding-right: 1ch; text-align: right; color: var(--ink-mute); user-select: none; } | 69 | & .ln { width: 6ch; min-width: 6ch; padding-right: 1ch; text-align: right; color: var(--ink-mute); user-select: none; } | ||
| @@ -75,13 +78,15 @@ | |||||
| 75 | & tr.del .ln, & .ln.del { color: var(--red); } | 78 | & tr.del .ln, & .ln.del { color: var(--red); } | ||
| 76 | & .none { background: var(--paper-alt); } | 79 | & .none { background: var(--paper-alt); } | ||
| 77 | } | 80 | } | ||
| 78 | - | .dscroll { overflow-x: auto; border: 1px solid var(--rule); border-top: 0; } | 81 | + | .dscroll { overflow-x: auto; background: var(--bg-surface); border: 1px solid var(--border); border-top: 0; border-radius: 0 0 var(--r) var(--r); } |
| 79 | .spl { width: 100%; table-layout: fixed; & .tx { overflow: hidden; text-overflow: ellipsis; } & .ln { width: 6ch; } & .m { width: 2ch; } } | 82 | .spl { width: 100%; table-layout: fixed; & .tx { overflow: hidden; text-overflow: ellipsis; } & .ln { width: 6ch; } & .m { width: 2ch; } } | ||
| 80 | #diffs[data-diff="unified"] .spl, #diffs[data-diff="split"] .uni { display: none; } | 83 | #diffs[data-diff="unified"] .spl, #diffs[data-diff="split"] .uni { display: none; } | ||
| 81 | 84 | ||||
| 82 | /* diff table of contents in the rail */ | 85 | /* diff table of contents in the rail */ | ||
| 83 | .files { | 86 | .files { | ||
| 84 | - | & a { display: grid; grid-template-columns: 2ch minmax(0, 1fr) auto; column-gap: 1ch; text-decoration: none; } | 87 | + | font-size: var(--fs-s); |
| 88 | + | & a { display: grid; grid-template-columns: 2ch minmax(0, 1fr) auto; column-gap: var(--s2); padding: 2px var(--s2); margin: 0 calc(-1 * var(--s2)); border-radius: 4px; text-decoration: none; font-family: var(--mono); } | |||
| 89 | + | & a:hover { background: var(--bg-raised); } | |||
| 85 | & a:hover .p { text-decoration: underline; } | 90 | & a:hover .p { text-decoration: underline; } | ||
| 86 | & a[aria-current="true"] { background: var(--row); font-weight: bold; } | 91 | & a[aria-current="true"] { background: var(--row); font-weight: bold; } | ||
| 87 | & .p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; } | 92 | & .p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; } | ||
| @@ -90,26 +95,30 @@ | |||||
| 90 | } | 95 | } | ||
| 91 | 96 | ||||
| 92 | @media (max-width: 700px) { | 97 | @media (max-width: 700px) { | ||
| 93 | - | .diff-tools { flex-wrap: wrap; column-gap: 1ch; } | |||
| 94 | .diff-tools .grow { flex-basis: 100%; height: 0; } | 98 | .diff-tools .grow { flex-basis: 100%; height: 0; } | ||
| 95 | - | .show-kv dd { overflow-wrap: anywhere; } | 99 | + | .file-h .acts { margin-left: 0; } |
| 96 | - | .file-h .acts { gap: 1ch; } | |||
| 97 | } | 100 | } | ||
| 98 | - | .pager { display: flex; gap: 3ch; justify-content: center; margin-top: var(--lh); } | 101 | + | .pager { display: flex; gap: var(--s3); justify-content: center; } |
| 99 | .days section[hidden] { display: none; } | 102 | .days section[hidden] { display: none; } | ||
| 100 | .list.refs { | 103 | .list.refs { | ||
| 101 | - | & .c-kind { width: 8ch; } | 104 | + | & .c-kind { width: 80px; color: var(--ink-mute); } |
| 102 | - | & .c-name { width: 34ch; } | 105 | + | & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); } |
| 103 | - | & .c-info { width: 11ch; } | 106 | + | & .c-sha { width: 96px; } |
| 104 | - | & .c-sha { width: 9ch; } | |||
| 105 | & .c-sub { width: auto; } | 107 | & .c-sub { width: auto; } | ||
| 106 | } | 108 | } | ||
| 109 | + | /* the commit table inside a rail block: no frame of its own */ | |||
| 107 | .rail .list.commits { | 110 | .rail .list.commits { | ||
| 111 | + | border: 0; border-radius: 0; background: none; | |||
| 112 | + | & td { height: 34px; padding: 0 var(--s2); } | |||
| 113 | + | & td.cur { width: 3px; padding: 0; } | |||
| 114 | + | & td:nth-child(2) { padding-left: var(--s3); } | |||
| 115 | + | & td:last-child { padding-right: var(--s4); } | |||
| 108 | & .c-who { display: none; } | 116 | & .c-who { display: none; } | ||
| 109 | - | & .c-sha { width: 8ch; } | 117 | + | & .c-sha { width: 72px; } |
| 110 | - | & .c-when { width: 9ch; padding-right: 0; text-align: right; } | 118 | + | & .c-subject { font-size: var(--fs-s); } |
| 119 | + | & .c-subject .pill { display: none; } | |||
| 120 | + | & .c-when { width: 70px; text-align: right; } | |||
| 111 | } | 121 | } | ||
| 112 | - | .rail p + p { margin-top: 0; } | |||
| 113 | .rail section > p { overflow-wrap: anywhere; } | 122 | .rail section > p { overflow-wrap: anywhere; } | ||
| 114 | @media (max-width: 700px) { | 123 | @media (max-width: 700px) { | ||
| 115 | /* the name takes what the fixed columns leave */ | 124 | /* the name takes what the fixed columns leave */ | ||
| @@ -118,7 +127,7 @@ | |||||
| 118 | } | 127 | } | ||
| 119 | 128 | ||||
| 120 | @layer components { | 129 | @layer components { | ||
| 121 | - | .gallery section { margin-top: calc(var(--lh) * 2); } | 130 | + | .gallery section { margin-top: var(--s6); } |
| 122 | .gallery .rule-h > :last-child { font-weight: normal; color: var(--ink-mute); order: 2; } | 131 | .gallery .rule-h > :last-child { font-weight: normal; color: var(--ink-mute); order: 2; } | ||
| 123 | .gallery .rule-h::after { order: 1; } | 132 | .gallery .rule-h::after { order: 1; } | ||
| 124 | .gallery .files { max-width: var(--rail); } | 133 | .gallery .files { max-width: var(--rail); } | ||
M internal/adapters/web/static/css/55-diff.css +1 -6
| @@ -2,12 +2,7 @@ | |||||
| 2 | /* an invisible character in code, shown instead of applied (Trojan Source) */ | 2 | /* an invisible character in code, shown instead of applied (Trojan Source) */ | ||
| 3 | .inv { | 3 | .inv { | ||
| 4 | color: var(--red); font-weight: bold; font-size: var(--fs-s); | 4 | color: var(--red); font-weight: bold; font-size: var(--fs-s); | ||
| 5 | - | outline: 1px solid var(--red); outline-offset: -1px; padding: 0 .25ch; | 5 | + | outline: 1px solid var(--red); outline-offset: -1px; padding: 0 2px; |
| 6 | } | 6 | } | ||
| 7 | .eol { color: var(--ink-mute); padding-left: 1ch; user-select: none; } | 7 | .eol { color: var(--ink-mute); padding-left: 1ch; user-select: none; } | ||
| 8 | - | /* the layout links of the diff tools read as the buttons of the mockup */ | |||
| 9 | - | .diff-tools a.vbtn { color: var(--ink-dim); text-decoration: none; } | |||
| 10 | - | .diff-tools a.vbtn:hover { color: var(--ink); text-decoration: underline; } | |||
| 11 | - | .diff-tools a.vbtn[aria-current="true"] { color: var(--ink-hi); font-weight: bold; } | |||
| 12 | - | .diff-tools a.vbtn[aria-current="true"]::before { content: "▸"; } | |||
| 13 | } | 8 | } | ||
M internal/adapters/web/static/css/60-mr.css +41 -45
| @@ -1,90 +1,86 @@ | |||||
| 1 | @layer components { | 1 | @layer components { | ||
| 2 | /* merge requests: the review page. Hue only where it carries a verdict. */ | 2 | /* merge requests: the review page. Hue only where it carries a verdict. */ | ||
| 3 | .mr-h .show-kv { margin-top: 0; } | 3 | .mr-h .show-kv { margin-top: 0; } | ||
| 4 | - | .mr-h + * { margin-top: var(--lh); } | 4 | + | |
| 5 | .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); } | 5 | .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); } | ||
| 6 | .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); } | 6 | .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); } | ||
| 7 | .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); } | 7 | .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); } | ||
| 8 | - | .mr-desc { margin-top: var(--lh); max-width: var(--measure); white-space: pre-wrap; overflow-wrap: anywhere; } | 8 | + | .mr-desc { max-width: var(--measure); white-space: pre-wrap; overflow-wrap: anywhere; padding: var(--s4) var(--s5); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); } |
| 9 | .mr-desc a, .c-text a { text-decoration: underline; } | 9 | .mr-desc a, .c-text a { text-decoration: underline; } | ||
| 10 | 10 | ||||
| 11 | .since { | 11 | .since { | ||
| 12 | - | display: flex; flex-wrap: wrap; column-gap: 3ch; margin-top: var(--lh); | 12 | + | display: inline-flex; flex-wrap: wrap; gap: var(--s1); padding: var(--s1); |
| 13 | - | & a { text-decoration: none; color: var(--ink-dim); } | 13 | + | background: var(--bg-raised); border-radius: var(--r-s); |
| 14 | - | & a:hover { color: var(--ink); text-decoration: underline; } | 14 | + | & a { padding: var(--s1) var(--s3); border-radius: 4px; text-decoration: none; color: var(--ink-dim); font-size: var(--fs-s); } |
| 15 | - | & a[aria-current] { color: var(--ink-hi); font-weight: bold; } | 15 | + | & a:hover { color: var(--ink-hi); } |
| 16 | - | & a[aria-current]::before { content: "▸ "; } | 16 | + | & a[aria-current] { color: var(--ink-hi); font-weight: 700; background: var(--bg-surface); box-shadow: 0 0 0 1px var(--border-strong); } |
| 17 | - | & .mute { font-weight: normal; } | 17 | + | & .mute { font-weight: 400; font-family: var(--mono); } |
| 18 | } | 18 | } | ||
| 19 | 19 | ||||
| 20 | /* merge block: first thing in the rail */ | 20 | /* merge block: first thing in the rail */ | ||
| 21 | .decide { | 21 | .decide { | ||
| 22 | - | & .d-acts { margin-top: calc(var(--lh) / 2); } | 22 | + | border-color: var(--border-strong) !important; |
| 23 | & select { | 23 | & select { | ||
| 24 | - | display: block; width: 100%; font: inherit; color: inherit; background: var(--paper-alt); | 24 | + | display: block; width: 100%; height: 36px; padding: 0 var(--s3); |
| 25 | - | border: 1px solid var(--rule); border-radius: 0; padding: 0 1ch; | 25 | + | font: inherit; color: inherit; background: var(--bg-inset); |
| 26 | + | border: 1px solid var(--border-strong); border-radius: var(--r-s); | |||
| 26 | } | 27 | } | ||
| 27 | - | & .d-check { display: block; margin-top: 4px; color: var(--ink-dim); cursor: pointer; } | 28 | + | & .d-check { display: flex; gap: var(--s2); align-items: center; margin-top: var(--s2); color: var(--ink-dim); font-size: var(--fs-s); cursor: pointer; } |
| 28 | - | & .d-check input { accent-color: var(--ink); margin: 0 1ch 0 0; vertical-align: middle; } | 29 | + | & .d-check input { accent-color: var(--ink-hi); margin: 0; } |
| 29 | - | & .d-btns { display: flex; gap: 2ch; margin-top: 4px; flex-wrap: wrap; } | 30 | + | & .d-btns { display: flex; gap: var(--s2); margin-top: var(--s3); flex-wrap: wrap; } |
| 30 | - | & .d-btns button { color: var(--ink-dim); } | 31 | + | & .d-btns .primary { flex: 1; justify-content: center; } |
| 31 | - | & .d-btns button:hover { color: var(--ink); text-decoration: underline; } | 32 | + | & .d-or { font-size: var(--fs-s); } |
| 32 | - | & .d-btns .d-main { background: var(--ink); color: var(--paper); font-weight: bold; padding: 0 1ch; } | |||
| 33 | - | & .d-btns .d-main:hover { background: var(--ink-hi); text-decoration: none; } | |||
| 34 | - | & .d-or { margin-top: calc(var(--lh) / 2); } | |||
| 35 | & .clone { margin-top: 0; } | 33 | & .clone { margin-top: 0; } | ||
| 36 | & .clone code { white-space: pre-wrap; overflow-wrap: anywhere; } | 34 | & .clone code { white-space: pre-wrap; overflow-wrap: anywhere; } | ||
| 37 | } | 35 | } | ||
| 38 | 36 | ||||
| 39 | - | .pcommits li { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | 37 | + | .pcommits { font-size: var(--fs-s); display: grid; gap: var(--s1); } |
| 40 | - | .pcommits li[data-new] { background: var(--row); } | 38 | + | .pcommits li { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 2px var(--s2); margin: 0 calc(-1 * var(--s2)); border-radius: 4px; } |
| 39 | + | .pcommits li a { font-family: var(--mono); } | |||
| 40 | + | .pcommits li[data-new] { background: var(--bg-raised); } | |||
| 41 | .pcommits .s { color: var(--ink); } | 41 | .pcommits .s { color: var(--ink); } | ||
| 42 | - | .rail code { border-bottom: 1px dotted var(--ink-mute); } | 42 | + | |
| 43 | 43 | ||||
| 44 | /* a note: a ruled block, under its line in the diff and in the list of comments */ | 44 | /* a note: a ruled block, under its line in the diff and in the list of comments */ | ||
| 45 | .cmt { | 45 | .cmt { | ||
| 46 | - | border-left: 2px solid var(--ink); padding-left: 1ch; margin-top: calc(var(--lh) / 2); max-width: var(--measure); | 46 | + | padding: var(--s3) var(--s4); margin-top: var(--s3); max-width: var(--measure); |
| 47 | - | white-space: normal; | 47 | + | background: var(--bg-surface); border: 1px solid var(--border); border-left: 3px solid var(--ink-hi); border-radius: var(--r-s); |
| 48 | + | white-space: normal; font-family: var(--font); font-size: var(--fs-s); line-height: 22px; | |||
| 48 | & p { white-space: normal; } | 49 | & p { white-space: normal; } | ||
| 49 | & .c-who { font-weight: bold; } | 50 | & .c-who { font-weight: bold; } | ||
| 50 | & .c-state { color: var(--ink-dim); } | 51 | & .c-state { color: var(--ink-dim); } | ||
| 51 | & .c-text { white-space: pre-wrap; overflow-wrap: anywhere; } | 52 | & .c-text { white-space: pre-wrap; overflow-wrap: anywhere; } | ||
| 52 | & .c-where { font-size: var(--fs-s); overflow-wrap: anywhere; } | 53 | & .c-where { font-size: var(--fs-s); overflow-wrap: anywhere; } | ||
| 53 | - | & .c-act button { color: var(--ink-dim); } | 54 | + | & .c-act { margin-top: var(--s2); } |
| 54 | - | & .c-act button:hover { color: var(--ink); text-decoration: underline; } | 55 | + | & .c-act button { height: 26px; font-size: var(--fs-xs); } |
| 55 | - | &[data-resolved="true"] { border-left-color: var(--rule); } | 56 | + | &[data-resolved="true"] { border-left-color: var(--border-strong); } |
| 56 | &[data-resolved="true"] .c-text { color: var(--ink-dim); } | 57 | &[data-resolved="true"] .c-text { color: var(--ink-dim); } | ||
| 57 | } | 58 | } | ||
| 58 | - | .dt .fnote td { background: var(--paper-alt); padding: 2px 0 4px; white-space: normal; } | 59 | + | .dt .fnote td { background: var(--bg-canvas); padding: var(--s2) var(--s3); white-space: normal; } |
| 59 | - | .dt .fnote .cmt, .dt .fnote .cform { margin: 0; font-size: var(--fs); line-height: var(--lh); } | 60 | + | .dt .fnote .cmt, .dt .fnote .cform { margin: 0; } |
| 60 | - | .dt .fnote .cmt + .cmt, .dt .fnote .cmt + .cform { margin-top: calc(var(--lh) / 2); } | 61 | + | .dt .fnote .cmt + .cmt, .dt .fnote .cmt + .cform { margin-top: var(--s2); } |
| 61 | .ln a { color: inherit; text-decoration: none; } | 62 | .ln a { color: inherit; text-decoration: none; } | ||
| 62 | .ln a:hover, .ln a:focus-visible { text-decoration: underline; color: var(--ink-hi); } | 63 | .ln a:hover, .ln a:focus-visible { text-decoration: underline; color: var(--ink-hi); } | ||
| 63 | 64 | ||||
| 64 | /* the forms: a comment, a new merge request */ | 65 | /* the forms: a comment, a new merge request */ | ||
| 65 | .cform { | 66 | .cform { | ||
| 66 | - | display: grid; gap: 2px; margin-top: var(--lh); max-width: var(--measure); | 67 | + | display: grid; gap: var(--s2); margin-top: var(--s3); max-width: var(--measure); |
| 67 | - | & label { color: var(--ink-dim); } | 68 | + | font-family: var(--font); font-size: var(--fs-s); |
| 69 | + | & label { color: var(--ink-dim); font-weight: 600; } | |||
| 68 | & textarea, & input[type="text"] { | 70 | & textarea, & input[type="text"] { | ||
| 69 | - | display: block; width: 100%; font: inherit; color: inherit; resize: vertical; | 71 | + | display: block; width: 100%; font: inherit; font-size: var(--fs); color: inherit; resize: vertical; |
| 70 | - | background: var(--paper-alt); border: 1px solid var(--rule); border-radius: 0; padding: 0 1ch; | 72 | + | background: var(--bg-surface); border: 1px solid var(--border-strong); border-radius: var(--r-s); padding: var(--s2) var(--s3); |
| 71 | } | 73 | } | ||
| 72 | & textarea::placeholder { color: var(--ink-mute); opacity: 1; } | 74 | & textarea::placeholder { color: var(--ink-mute); opacity: 1; } | ||
| 73 | - | & .f-btns { display: flex; gap: 2ch; align-items: baseline; margin-top: 4px; } | 75 | + | & .f-btns { display: flex; gap: var(--s2); align-items: center; margin-top: var(--s1); } |
| 74 | - | & .f-btns a { color: var(--ink-dim); } | |||
| 75 | - | & .f-btns button { color: var(--ink-dim); } | |||
| 76 | - | & .f-btns button:hover { color: var(--ink); text-decoration: underline; } | |||
| 77 | - | & .f-btns .f-main { background: var(--ink); color: var(--paper); font-weight: bold; padding: 0 1ch; } | |||
| 78 | - | & .f-btns .f-main:hover { background: var(--ink-hi); text-decoration: none; } | |||
| 79 | } | 76 | } | ||
| 80 | - | .cform.create { margin-top: var(--lh); } | |||
| 81 | 77 | ||||
| 82 | .list.mrs { | 78 | .list.mrs { | ||
| 83 | - | & .c-n { width: 5ch; } | 79 | + | & .c-n { width: 64px; } |
| 84 | - | & .c-state { width: 9ch; } | 80 | + | & .c-state { width: 110px; } |
| 85 | & .c-subject { overflow: hidden; text-overflow: ellipsis; } | 81 | & .c-subject { overflow: hidden; text-overflow: ellipsis; } | ||
| 86 | - | & .c-who { width: 12ch; } | 82 | + | & .c-who { width: 140px; } |
| 87 | - | & .c-when { width: 11ch; } | 83 | + | & .c-when { width: 100px; } |
| 88 | & tr[data-state="open"] .c-state { font-weight: bold; } | 84 | & tr[data-state="open"] .c-state { font-weight: bold; } | ||
| 89 | & tr[data-state="open"] .c-state::before { content: "● "; color: var(--teal); } | 85 | & tr[data-state="open"] .c-state::before { content: "● "; color: var(--teal); } | ||
| 90 | & tr[data-state="merged"] .c-state::before { content: "✓ "; color: var(--teal); } | 86 | & tr[data-state="merged"] .c-state::before { content: "✓ "; color: var(--teal); } | ||
D internal/adapters/web/static/css/70-footer.css +0 -12
| @@ -1,12 +0,0 @@ | |||||
| 1 | - | @layer components { | |||
| 2 | - | /* AGPL §13: the version and the link to the source sit in the status bar, always visible */ | |||
| 3 | - | .status .st-src { | |||
| 4 | - | flex: none; | |||
| 5 | - | & a { color: var(--bar-ink); text-decoration: underline; text-decoration-color: var(--bar-dim); text-underline-offset: 3px; } | |||
| 6 | - | & a:hover { color: #fff; text-decoration-color: currentColor; } | |||
| 7 | - | } | |||
| 8 | - | @media (max-width: 700px) { | |||
| 9 | - | .status .st-ver { display: none; } | |||
| 10 | - | .status .st-hints { display: none; } | |||
| 11 | - | } | |||
| 12 | - | } | |||
M internal/adapters/web/static/css/80-live.css +5 -2
| @@ -1,6 +1,9 @@ | |||||
| 1 | @layer components { | 1 | @layer components { | ||
| 2 | /* live updates: the banner of new commits, an empty slot takes no room */ | 2 | /* live updates: the banner of new commits, an empty slot takes no room */ | ||
| 3 | #commits-head:empty { display: none; } | 3 | #commits-head:empty { display: none; } | ||
| 4 | - | .live-banner { margin: var(--lh) 0; padding: 0 1ch; background: var(--row); font-weight: bold; } | 4 | + | .live-banner { |
| 5 | - | .live-banner a { color: var(--ink); } | 5 | + | padding: var(--s3) var(--s4); font-weight: 700; |
| 6 | + | background: var(--bg-surface); border: 1px solid var(--border-strong); border-left: 3px solid var(--teal); border-radius: var(--r-s); | |||
| 7 | + | } | |||
| 8 | + | .live-banner a { color: var(--ink-hi); } | |||
| 6 | } | 9 | } | ||
A internal/adapters/web/static/fonts/OFL-mono.txt +93 -0
| @@ -0,0 +1,93 @@ | |||||
| 1 | + | Copyright 2020-2024 The Atkinson Hyperlegible Mono Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next-mono) | |||
| 2 | + | ||||
| 3 | + | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |||
| 4 | + | This license is copied below, and is also available with a FAQ at: | |||
| 5 | + | https://openfontlicense.org | |||
| 6 | + | ||||
| 7 | + | ||||
| 8 | + | ----------------------------------------------------------- | |||
| 9 | + | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |||
| 10 | + | ----------------------------------------------------------- | |||
| 11 | + | ||||
| 12 | + | PREAMBLE | |||
| 13 | + | The goals of the Open Font License (OFL) are to stimulate worldwide | |||
| 14 | + | development of collaborative font projects, to support the font creation | |||
| 15 | + | efforts of academic and linguistic communities, and to provide a free and | |||
| 16 | + | open framework in which fonts may be shared and improved in partnership | |||
| 17 | + | with others. | |||
| 18 | + | ||||
| 19 | + | The OFL allows the licensed fonts to be used, studied, modified and | |||
| 20 | + | redistributed freely as long as they are not sold by themselves. The | |||
| 21 | + | fonts, including any derivative works, can be bundled, embedded, | |||
| 22 | + | redistributed and/or sold with any software provided that any reserved | |||
| 23 | + | names are not used by derivative works. The fonts and derivatives, | |||
| 24 | + | however, cannot be released under any other type of license. The | |||
| 25 | + | requirement for fonts to remain under this license does not apply | |||
| 26 | + | to any document created using the fonts or their derivatives. | |||
| 27 | + | ||||
| 28 | + | DEFINITIONS | |||
| 29 | + | "Font Software" refers to the set of files released by the Copyright | |||
| 30 | + | Holder(s) under this license and clearly marked as such. This may | |||
| 31 | + | include source files, build scripts and documentation. | |||
| 32 | + | ||||
| 33 | + | "Reserved Font Name" refers to any names specified as such after the | |||
| 34 | + | copyright statement(s). | |||
| 35 | + | ||||
| 36 | + | "Original Version" refers to the collection of Font Software components as | |||
| 37 | + | distributed by the Copyright Holder(s). | |||
| 38 | + | ||||
| 39 | + | "Modified Version" refers to any derivative made by adding to, deleting, | |||
| 40 | + | or substituting -- in part or in whole -- any of the components of the | |||
| 41 | + | Original Version, by changing formats or by porting the Font Software to a | |||
| 42 | + | new environment. | |||
| 43 | + | ||||
| 44 | + | "Author" refers to any designer, engineer, programmer, technical | |||
| 45 | + | writer or other person who contributed to the Font Software. | |||
| 46 | + | ||||
| 47 | + | PERMISSION & CONDITIONS | |||
| 48 | + | Permission is hereby granted, free of charge, to any person obtaining | |||
| 49 | + | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |||
| 50 | + | redistribute, and sell modified and unmodified copies of the Font | |||
| 51 | + | Software, subject to the following conditions: | |||
| 52 | + | ||||
| 53 | + | 1) Neither the Font Software nor any of its individual components, | |||
| 54 | + | in Original or Modified Versions, may be sold by itself. | |||
| 55 | + | ||||
| 56 | + | 2) Original or Modified Versions of the Font Software may be bundled, | |||
| 57 | + | redistributed and/or sold with any software, provided that each copy | |||
| 58 | + | contains the above copyright notice and this license. These can be | |||
| 59 | + | included either as stand-alone text files, human-readable headers or | |||
| 60 | + | in the appropriate machine-readable metadata fields within text or | |||
| 61 | + | binary files as long as those fields can be easily viewed by the user. | |||
| 62 | + | ||||
| 63 | + | 3) No Modified Version of the Font Software may use the Reserved Font | |||
| 64 | + | Name(s) unless explicit written permission is granted by the corresponding | |||
| 65 | + | Copyright Holder. This restriction only applies to the primary font name as | |||
| 66 | + | presented to the users. | |||
| 67 | + | ||||
| 68 | + | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |||
| 69 | + | Software shall not be used to promote, endorse or advertise any | |||
| 70 | + | Modified Version, except to acknowledge the contribution(s) of the | |||
| 71 | + | Copyright Holder(s) and the Author(s) or with their explicit written | |||
| 72 | + | permission. | |||
| 73 | + | ||||
| 74 | + | 5) The Font Software, modified or unmodified, in part or in whole, | |||
| 75 | + | must be distributed entirely under this license, and must not be | |||
| 76 | + | distributed under any other license. The requirement for fonts to | |||
| 77 | + | remain under this license does not apply to any document created | |||
| 78 | + | using the Font Software. | |||
| 79 | + | ||||
| 80 | + | TERMINATION | |||
| 81 | + | This license becomes null and void if any of the above conditions are | |||
| 82 | + | not met. | |||
| 83 | + | ||||
| 84 | + | DISCLAIMER | |||
| 85 | + | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |||
| 86 | + | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |||
| 87 | + | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |||
| 88 | + | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |||
| 89 | + | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |||
| 90 | + | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |||
| 91 | + | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |||
| 92 | + | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |||
| 93 | + | OTHER DEALINGS IN THE FONT SOFTWARE. | |||
A internal/adapters/web/static/fonts/OFL-next.txt +93 -0
| @@ -0,0 +1,93 @@ | |||||
| 1 | + | Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next) | |||
| 2 | + | ||||
| 3 | + | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |||
| 4 | + | This license is copied below, and is also available with a FAQ at: | |||
| 5 | + | https://openfontlicense.org | |||
| 6 | + | ||||
| 7 | + | ||||
| 8 | + | ----------------------------------------------------------- | |||
| 9 | + | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |||
| 10 | + | ----------------------------------------------------------- | |||
| 11 | + | ||||
| 12 | + | PREAMBLE | |||
| 13 | + | The goals of the Open Font License (OFL) are to stimulate worldwide | |||
| 14 | + | development of collaborative font projects, to support the font creation | |||
| 15 | + | efforts of academic and linguistic communities, and to provide a free and | |||
| 16 | + | open framework in which fonts may be shared and improved in partnership | |||
| 17 | + | with others. | |||
| 18 | + | ||||
| 19 | + | The OFL allows the licensed fonts to be used, studied, modified and | |||
| 20 | + | redistributed freely as long as they are not sold by themselves. The | |||
| 21 | + | fonts, including any derivative works, can be bundled, embedded, | |||
| 22 | + | redistributed and/or sold with any software provided that any reserved | |||
| 23 | + | names are not used by derivative works. The fonts and derivatives, | |||
| 24 | + | however, cannot be released under any other type of license. The | |||
| 25 | + | requirement for fonts to remain under this license does not apply | |||
| 26 | + | to any document created using the fonts or their derivatives. | |||
| 27 | + | ||||
| 28 | + | DEFINITIONS | |||
| 29 | + | "Font Software" refers to the set of files released by the Copyright | |||
| 30 | + | Holder(s) under this license and clearly marked as such. This may | |||
| 31 | + | include source files, build scripts and documentation. | |||
| 32 | + | ||||
| 33 | + | "Reserved Font Name" refers to any names specified as such after the | |||
| 34 | + | copyright statement(s). | |||
| 35 | + | ||||
| 36 | + | "Original Version" refers to the collection of Font Software components as | |||
| 37 | + | distributed by the Copyright Holder(s). | |||
| 38 | + | ||||
| 39 | + | "Modified Version" refers to any derivative made by adding to, deleting, | |||
| 40 | + | or substituting -- in part or in whole -- any of the components of the | |||
| 41 | + | Original Version, by changing formats or by porting the Font Software to a | |||
| 42 | + | new environment. | |||
| 43 | + | ||||
| 44 | + | "Author" refers to any designer, engineer, programmer, technical | |||
| 45 | + | writer or other person who contributed to the Font Software. | |||
| 46 | + | ||||
| 47 | + | PERMISSION & CONDITIONS | |||
| 48 | + | Permission is hereby granted, free of charge, to any person obtaining | |||
| 49 | + | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |||
| 50 | + | redistribute, and sell modified and unmodified copies of the Font | |||
| 51 | + | Software, subject to the following conditions: | |||
| 52 | + | ||||
| 53 | + | 1) Neither the Font Software nor any of its individual components, | |||
| 54 | + | in Original or Modified Versions, may be sold by itself. | |||
| 55 | + | ||||
| 56 | + | 2) Original or Modified Versions of the Font Software may be bundled, | |||
| 57 | + | redistributed and/or sold with any software, provided that each copy | |||
| 58 | + | contains the above copyright notice and this license. These can be | |||
| 59 | + | included either as stand-alone text files, human-readable headers or | |||
| 60 | + | in the appropriate machine-readable metadata fields within text or | |||
| 61 | + | binary files as long as those fields can be easily viewed by the user. | |||
| 62 | + | ||||
| 63 | + | 3) No Modified Version of the Font Software may use the Reserved Font | |||
| 64 | + | Name(s) unless explicit written permission is granted by the corresponding | |||
| 65 | + | Copyright Holder. This restriction only applies to the primary font name as | |||
| 66 | + | presented to the users. | |||
| 67 | + | ||||
| 68 | + | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |||
| 69 | + | Software shall not be used to promote, endorse or advertise any | |||
| 70 | + | Modified Version, except to acknowledge the contribution(s) of the | |||
| 71 | + | Copyright Holder(s) and the Author(s) or with their explicit written | |||
| 72 | + | permission. | |||
| 73 | + | ||||
| 74 | + | 5) The Font Software, modified or unmodified, in part or in whole, | |||
| 75 | + | must be distributed entirely under this license, and must not be | |||
| 76 | + | distributed under any other license. The requirement for fonts to | |||
| 77 | + | remain under this license does not apply to any document created | |||
| 78 | + | using the Font Software. | |||
| 79 | + | ||||
| 80 | + | TERMINATION | |||
| 81 | + | This license becomes null and void if any of the above conditions are | |||
| 82 | + | not met. | |||
| 83 | + | ||||
| 84 | + | DISCLAIMER | |||
| 85 | + | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |||
| 86 | + | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |||
| 87 | + | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |||
| 88 | + | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |||
| 89 | + | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |||
| 90 | + | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |||
| 91 | + | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |||
| 92 | + | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |||
| 93 | + | OTHER DEALINGS IN THE FONT SOFTWARE. | |||
A internal/adapters/web/static/fonts/atkinson-mono.woff2 +0 -0 binary file: no text diff
binary file: no text diff
A internal/adapters/web/static/fonts/atkinson-next-italic.woff2 +0 -0 binary file: no text diff
binary file: no text diff
A internal/adapters/web/static/fonts/atkinson-next.woff2 +0 -0 binary file: no text diff
binary file: no text diff
A internal/adapters/web/static/icons/branch.svg +1 -0
| @@ -0,0 +1,1 @@ | |||||
| 1 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="3.5" r="1.5"/><circle cx="5" cy="12.5" r="1.5"/><circle cx="11" cy="4.5" r="1.5"/><path d="M5 5v6M11 6c0 3.5-6 2-6 5"/></svg>⏎̸ | |||
A internal/adapters/web/static/icons/brand.svg +1 -0
| @@ -0,0 +1,1 @@ | |||||
| 1 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="2" stroke-linecap="square"><path d="M4 4h8M4 8h6M4 12h8M4 4v8"/></svg>⏎̸ | |||
A internal/adapters/web/static/icons/file.svg +1 -0
| @@ -0,0 +1,1 @@ | |||||
| 1 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"><path d="M3.5 1.5h6l3 3v10h-9zM9.5 1.5v3h3M6 8h4M6 11h4"/></svg>⏎̸ | |||
A internal/adapters/web/static/icons/search.svg +1 -0
| @@ -0,0 +1,1 @@ | |||||
| 1 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round"><circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3.5 3.5"/></svg>⏎̸ | |||
M internal/adapters/web/static/js/ermite.js +2 -0
| @@ -9,6 +9,8 @@ | |||||
| 9 | let pending = 0; | 9 | let pending = 0; | ||
| 10 | const fire = (name) => document.body.dispatchEvent(new CustomEvent('k-' + name, { bubbles: false })); | 10 | const fire = (name) => document.body.dispatchEvent(new CustomEvent('k-' + name, { bubbles: false })); | ||
| 11 | addEventListener('keydown', (e) => { | 11 | addEventListener('keydown', (e) => { | ||
| 12 | + | // ctrl k / cmd k opens the palette from anywhere, inputs included. | |||
| 13 | + | if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === 'k') { e.preventDefault(); pending = 0; return fire('cmd'); } | |||
| 12 | if (e.ctrlKey || e.metaKey || e.altKey) return; | 14 | if (e.ctrlKey || e.metaKey || e.altKey) return; | ||
| 13 | const typing = e.target.closest('input, textarea, select, [contenteditable]'); | 15 | const typing = e.target.closest('input, textarea, select, [contenteditable]'); | ||
| 14 | if (e.key === 'Escape') { pending = 0; if (typing) e.target.blur(); return fire('esc'); } | 16 | if (e.key === 'Escape') { pending = 0; if (typing) e.target.blur(); return fire('esc'); } | ||
A internal/adapters/web/static/rules.json +16 -0
| @@ -0,0 +1,16 @@ | |||||
| 1 | + | { | |||
| 2 | + | "prefetch": [ | |||
| 3 | + | { | |||
| 4 | + | "source": "document", | |||
| 5 | + | "where": { | |||
| 6 | + | "and": [ | |||
| 7 | + | { "href_matches": "/*" }, | |||
| 8 | + | { "not": { "href_matches": "/-/*" } }, | |||
| 9 | + | { "not": { "href_matches": "/*/raw/*" } }, | |||
| 10 | + | { "not": { "selector_matches": "[rel~=nofollow]" } } | |||
| 11 | + | ] | |||
| 12 | + | }, | |||
| 13 | + | "eagerness": "moderate" | |||
| 14 | + | } | |||
| 15 | + | ] | |||
| 16 | + | } | |||
M internal/adapters/web/summary.go +1 -5
| @@ -73,11 +73,7 @@ func (s *Server) summary(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 73 | l := s.repoLayout(ctx, d, "summary") | 73 | l := s.repoLayout(ctx, d, "summary") | ||
| 74 | l.Title = d.name() + " · " + s.instance | 74 | l.Title = d.name() + " · " + s.instance | ||
| 75 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=summary") | 75 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=summary") | ||
| 76 | - | l.Status = Status{ | 76 | + | l.HasRail = true |
| 77 | - | Mode: "summary", Context: plural(len(commits.Msg.GetCommits()), "recent commit", "recent commits"), | |||
| 78 | - | Hints: "j/k move · enter open · b ref · y permalink · ? help", Cursor: true, Count: len(p.Recent), | |||
| 79 | - | } | |||
| 80 | - | l.HasRail, l.Read, l.RailSum = true, true, "clone · facts · recent commits · outline" | |||
| 81 | return Summary(l, p), http.StatusOK, nil | 77 | return Summary(l, p), http.StatusOK, nil | ||
| 82 | } | 78 | } | ||
| 83 | 79 | ||||
M internal/adapters/web/tree.go +0 -4
| @@ -103,9 +103,5 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in | |||||
| 103 | } else { | 103 | } else { | ||
| 104 | l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance | 104 | l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance | ||
| 105 | } | 105 | } | ||
| 106 | - | l.Status = Status{ | |||
| 107 | - | Mode: "tree", Context: "tree · " + plural(len(entries), "entry", "entries"), | |||
| 108 | - | Hints: "j/k move · enter open · / filter · b ref · ? help", Cursor: true, Count: len(p.Rows), | |||
| 109 | - | } | |||
| 110 | return Tree(l, p), http.StatusOK, nil | 106 | return Tree(l, p), http.StatusOK, nil | ||
| 111 | } | 107 | } | ||
M internal/adapters/web/tree.templ +3 -4
| @@ -7,12 +7,12 @@ package web | |||||
| 7 | templ Tree(l Layout, p TreePage) { | 7 | templ Tree(l Layout, p TreePage) { | ||
| 8 | @Shell(l, nil) { | 8 | @Shell(l, nil) { | ||
| 9 | <div class="filter"> | 9 | <div class="filter"> | ||
| 10 | - | <label for="tree-filter">filter></label> | 10 | + | <label for="tree-filter" class="sr">filter</label> |
| 11 | <input | 11 | <input | ||
| 12 | id="tree-filter" | 12 | id="tree-filter" | ||
| 13 | type="search" | 13 | type="search" | ||
| 14 | data-filter | 14 | data-filter | ||
| 15 | - | placeholder="this directory ( / )" | 15 | + | placeholder="Filter this directory…" |
| 16 | autocomplete="off" | 16 | autocomplete="off" | ||
| 17 | hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')" | 17 | hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')" | ||
| 18 | /> | 18 | /> | ||
| @@ -34,8 +34,7 @@ templ Tree(l Layout, p TreePage) { | |||||
| 34 | <p class="note">{ n }</p> | 34 | <p class="note">{ n }</p> | ||
| 35 | } | 35 | } | ||
| 36 | if p.Readme != nil { | 36 | if p.Readme != nil { | ||
| 37 | - | <h2 class="rule-h"><span>{ p.Readme.Name }</span></h2> | 37 | + | @ReadmeCard(*p.Readme) |
| 38 | - | @ReadmeView(*p.Readme) | |||
| 39 | } | 38 | } | ||
| 40 | } | 39 | } | ||
| 41 | } | 40 | } | ||
M internal/adapters/web/tree_templ.go +68 -85 generated file: collapsed
M internal/adapters/web/view.go +5 -18
| @@ -7,31 +7,21 @@ import "html/template" | |||||
| 7 | // View models handed to the templ components. Components only loop and | 7 | // View models handed to the templ components. Components only loop and | ||
| 8 | // branch on these; the handlers build them from the API messages. | 8 | // branch on these; the handlers build them from the API messages. | ||
| 9 | 9 | ||||
| 10 | - | // Crumb is one segment of the path in the top bar. | 10 | + | // Crumb is one segment of the path in the header. |
| 11 | type Crumb struct { | 11 | type Crumb struct { | ||
| 12 | Label string | 12 | Label string | ||
| 13 | Href string | 13 | Href string | ||
| 14 | Current bool | 14 | Current bool | ||
| 15 | } | 15 | } | ||
| 16 | 16 | ||||
| 17 | - | // Section is a repository section in the top bar, reached by its key. | 17 | + | // Section is a repository tab under the header, reached by its key. |
| 18 | type Section struct { | 18 | type Section struct { | ||
| 19 | Key string // mnemonic key | 19 | Key string // mnemonic key | ||
| 20 | - | Post string // rest of the label after the key | 20 | + | Label string |
| 21 | Href string | 21 | Href string | ||
| 22 | Current bool | 22 | Current bool | ||
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | - | // Status is the content of the bottom bar. | |||
| 26 | - | type Status struct { | |||
| 27 | - | Mode string // "home", "summary", "refs"… | |||
| 28 | - | Context string // "3 repos · sort push ▼" | |||
| 29 | - | Hints string // "j/k move · enter open · ? help" | |||
| 30 | - | Cursor bool // show the row cursor position n/N | |||
| 31 | - | Blob bool // show the selected line range | |||
| 32 | - | Count int // number of rows under the cursor | |||
| 33 | - | } | |||
| 34 | - | ||||
| 35 | // RefItem is a branch or tag in the ref switcher. | 25 | // RefItem is a branch or tag in the ref switcher. | ||
| 36 | type RefItem struct { | 26 | type RefItem struct { | ||
| 37 | Name string | 27 | Name string | ||
| @@ -50,10 +40,9 @@ type Layout struct { | |||||
| 50 | SourceURL string | 40 | SourceURL string | ||
| 51 | Assets Assets | 41 | Assets Assets | ||
| 52 | Crumbs []Crumb | 42 | Crumbs []Crumb | ||
| 53 | - | Ref string // ref shown in the top bar, "" outside a repo view | 43 | + | Ref string // ref shown in the header, "" outside a repo view |
| 54 | Short string // short sha shown after the ref | 44 | Short string // short sha shown after the ref | ||
| 55 | Sections []Section | 45 | Sections []Section | ||
| 56 | - | Status Status | |||
| 57 | Repo string | 46 | Repo string | ||
| 58 | SHA string // full sha of the ref, for the permalink key | 47 | SHA string // full sha of the ref, for the permalink key | ||
| 59 | RefPath string // the URL segment that `y` rewrites: the ref name | 48 | RefPath string // the URL segment that `y` rewrites: the ref name | ||
| @@ -61,9 +50,7 @@ type Layout struct { | |||||
| 61 | Tags []RefItem | 50 | Tags []RefItem | ||
| 62 | Cmd []CmdItem | 51 | Cmd []CmdItem | ||
| 63 | HasRail bool | 52 | HasRail bool | ||
| 64 | - | RailSum string // the rail as one line when it folds above the content (< 1100px) | 53 | + | // Live connects the page to the event stream (/-/events). |
| 65 | - | Read bool // prose page: content capped at the reading measure | |||
| 66 | - | // Live connects the status bar to the event stream (/-/events). | |||
| 67 | Live bool | 54 | Live bool | ||
| 68 | // LiveURL is the stream the page follows, with its query. | 55 | // LiveURL is the stream the page follows, with its query. | ||
| 69 | LiveURL string | 56 | LiveURL string | ||
M internal/adapters/web/web.go +6 -5
| @@ -196,6 +196,9 @@ func (s *Server) write(ctx context.Context, w http.ResponseWriter, r *http.Reque | |||||
| 196 | } | 196 | } | ||
| 197 | h := w.Header() | 197 | h := w.Header() | ||
| 198 | h.Set("Content-Type", "text/html; charset=utf-8") | 198 | h.Set("Content-Type", "text/html; charset=utf-8") | ||
| 199 | + | // Links on this origin are prefetched on hover, so a click shows the page | |||
| 200 | + | // at once. Every GET page is safe to load (nothing is marked read on GET). | |||
| 201 | + | h.Set("Speculation-Rules", `"`+s.assets.urls.Rules+`"`) | |||
| 199 | // Public pages: the browser and any proxy revalidate each time. A page | 202 | // Public pages: the browser and any proxy revalidate each time. A page | ||
| 200 | // rendered for the owner is private and never stored; a handler that set | 203 | // rendered for the owner is private and never stored; a handler that set | ||
| 201 | // its own Cache-Control keeps it. | 204 | // its own Cache-Control keeps it. | ||
| @@ -253,18 +256,16 @@ func (s *Server) base() Layout { | |||||
| 253 | } | 256 | } | ||
| 254 | 257 | ||||
| 255 | // stateLayout is the layout of a state page: only the way home. | 258 | // stateLayout is the layout of a state page: only the way home. | ||
| 256 | - | func (s *Server) stateLayout(title, context string, crumbs []Crumb) Layout { | 259 | + | func (s *Server) stateLayout(title string, crumbs []Crumb) Layout { |
| 257 | l := s.base() | 260 | l := s.base() | ||
| 258 | l.Title = title + " · " + s.instance | 261 | l.Title = title + " · " + s.instance | ||
| 259 | l.Crumbs = crumbs | 262 | l.Crumbs = crumbs | ||
| 260 | - | l.Sections = []Section{{Key: "h", Post: "ome", Href: "/"}} | |||
| 261 | - | l.Status = Status{Mode: "state", Context: context, Hints: "g h home · ? help"} | |||
| 262 | l.Cmd = []CmdItem{{Kind: "go", Label: "home", Href: "/"}} | 263 | l.Cmd = []CmdItem{{Kind: "go", Label: "home", Href: "/"}} | ||
| 263 | return l | 264 | return l | ||
| 264 | } | 265 | } | ||
| 265 | 266 | ||||
| 266 | func (s *Server) notFoundPage() templ.Component { | 267 | func (s *Server) notFoundPage() templ.Component { | ||
| 267 | - | l := s.stateLayout("404", "404 · not found", []Crumb{{Label: "-"}, {Label: "404"}}) | 268 | + | l := s.stateLayout("404", []Crumb{{Label: "not found", Current: true}}) |
| 268 | return State(l, StatePage{ | 269 | return State(l, StatePage{ | ||
| 269 | Code: "404", Title: "not found", | 270 | Code: "404", Title: "not found", | ||
| 270 | Lines: []string{"no such page, ref or path.", "check the URL, or start from the repo list."}, | 271 | Lines: []string{"no such page, ref or path.", "check the URL, or start from the repo list."}, | ||
| @@ -273,7 +274,7 @@ func (s *Server) notFoundPage() templ.Component { | |||||
| 273 | } | 274 | } | ||
| 274 | 275 | ||||
| 275 | func (s *Server) errorPage(id string) templ.Component { | 276 | func (s *Server) errorPage(id string) templ.Component { | ||
| 276 | - | l := s.stateLayout("error", "500 · internal error", []Crumb{{Label: "-"}, {Label: "error"}}) | 277 | + | l := s.stateLayout("error", []Crumb{{Label: "error", Current: true}}) |
| 277 | return State(l, StatePage{ | 278 | return State(l, StatePage{ | ||
| 278 | Code: "500", Title: "internal error", | 279 | Code: "500", Title: "internal error", | ||
| 279 | Lines: []string{ | 280 | Lines: []string{ | ||