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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>data.bin · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,45 +42,62 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <h1 class="file-h"> | |
| 66 | 89 | <span>data.bin</span> | |
| 67 | 90 | <span class="acts"> | |
| 68 | - | <a href="/files/raw/main/data.bin">raw</a> | |
| 69 | - | <a href="/files/commits/main/data.bin">history</a> | |
| 70 | - | <a href="/files/tree/main">dir</a> | |
| 91 | + | <a class="btn" href="/files/raw/main/data.bin">Raw</a> | |
| 92 | + | <a class="btn" href="/files/commits/main/data.bin">History</a> | |
| 93 | + | <a class="btn" href="/files/tree/main">Folder</a> | |
| 71 | 94 | </span> | |
| 72 | 95 | </h1> | |
| 73 | 96 | <p class="notice" role="note">binary file (application/octet-stream, 10 B): not shown. use raw.</p> | |
| 74 | 97 | </main> | |
| 75 | 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 | 99 | <section aria-labelledby="r-file"> | |
| 79 | - | <h2 id="r-file">file</h2> | |
| 100 | + | <h2 id="r-file">File</h2> | |
| 80 | 101 | <dl class="kv"> | |
| 81 | 102 | <dt>size</dt> | |
| 82 | 103 | <dd>10 B</dd> | |
| @@ -85,80 +106,80 @@ | |||
| 85 | 106 | </dl> | |
| 86 | 107 | </section> | |
| 87 | 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 | 110 | </h2> | |
| 90 | 111 | <p> | |
| 91 | 112 | <a href="/files/commit/SHA40">SHA7</a> add data.bin</p> | |
| 92 | 113 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | |
| 93 | 114 | </p> | |
| 94 | 115 | </section> | |
| 95 | - | </details> | |
| 96 | 116 | </aside> | |
| 97 | 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 | 133 | <div class="panel-head"> | |
| 100 | - | <span>keys</span> | |
| 101 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 134 | + | <h2>Keyboard shortcuts</h2> | |
| 135 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 136 | + | <kbd>esc</kbd> close</button> | |
| 102 | 137 | </div> | |
| 103 | 138 | <dl class="keys"> | |
| 104 | 139 | <div> | |
| 105 | 140 | <dt> | |
| 106 | - | <kbd>j</kbd> | |
| 107 | - | <kbd>k</kbd> | |
| 141 | + | <kbd>ctrl k</kbd> | |
| 142 | + | <kbd>:</kbd> | |
| 108 | 143 | </dt> | |
| 109 | - | <dd>move cursor down / up</dd> | |
| 144 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 110 | 145 | </div> | |
| 111 | 146 | <div> | |
| 112 | 147 | <dt> | |
| 113 | - | <kbd>enter</kbd> | |
| 148 | + | <kbd>?</kbd> | |
| 114 | 149 | </dt> | |
| 115 | - | <dd>open row under cursor</dd> | |
| 150 | + | <dd>this list</dd> | |
| 116 | 151 | </div> | |
| 117 | 152 | <div> | |
| 118 | 153 | <dt> | |
| 119 | - | <kbd>g</kbd> | |
| 120 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 154 | + | <kbd>j</kbd> | |
| 155 | + | <kbd>k</kbd> | |
| 121 | 156 | </dt> | |
| 122 | - | <dd>first / last row</dd> | |
| 157 | + | <dd>move the row cursor down / up</dd> | |
| 123 | 158 | </div> | |
| 124 | 159 | <div> | |
| 125 | 160 | <dt> | |
| 126 | - | <kbd>/</kbd> | |
| 161 | + | <kbd>enter</kbd> | |
| 127 | 162 | </dt> | |
| 128 | - | <dd>focus the filter</dd> | |
| 163 | + | <dd>open the row under the cursor</dd> | |
| 129 | 164 | </div> | |
| 130 | 165 | <div> | |
| 131 | 166 | <dt> | |
| 132 | - | <kbd>g h</kbd> | |
| 133 | - | <kbd>g t</kbd> | |
| 167 | + | <kbd>g g</kbd> | |
| 168 | + | <kbd>G</kbd> | |
| 134 | 169 | </dt> | |
| 135 | - | <dd>go home / tree</dd> | |
| 170 | + | <dd>first / last row</dd> | |
| 136 | 171 | </div> | |
| 137 | 172 | <div> | |
| 138 | 173 | <dt> | |
| 139 | - | <kbd>g c</kbd> | |
| 140 | - | <kbd>g r</kbd> | |
| 174 | + | <kbd>/</kbd> | |
| 141 | 175 | </dt> | |
| 142 | - | <dd>go commits / refs</dd> | |
| 176 | + | <dd>filter the list</dd> | |
| 143 | 177 | </div> | |
| 144 | 178 | <div> | |
| 145 | 179 | <dt> | |
| 146 | 180 | <kbd>b</kbd> | |
| 147 | 181 | </dt> | |
| 148 | - | <dd>switch ref (branches, tags)</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> | |
| 182 | + | <dd>switch branch or tag</dd> | |
| 162 | 183 | </div> | |
| 163 | 184 | <div> | |
| 164 | 185 | <dt> | |
| @@ -166,15 +187,21 @@ | |||
| 166 | 187 | <kbd>t</kbd> | |
| 167 | 188 | <kbd>c</kbd> | |
| 168 | 189 | <kbd>r</kbd> | |
| 190 | + | <kbd>m</kbd> | |
| 169 | 191 | </dt> | |
| 170 | - | <dd>repo sections (when shown)</dd> | |
| 192 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 171 | 193 | </div> | |
| 172 | 194 | <div> | |
| 173 | 195 | <dt> | |
| 174 | - | <kbd>m</kbd> | |
| 175 | - | <kbd>g m</kbd> | |
| 196 | + | <kbd>g h</kbd> | |
| 197 | + | </dt> | |
| 198 | + | <dd>home</dd> | |
| 199 | + | </div> | |
| 200 | + | <div> | |
| 201 | + | <dt> | |
| 202 | + | <kbd>y</kbd> | |
| 176 | 203 | </dt> | |
| 177 | - | <dd>merge requests (when shown)</dd> | |
| 204 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 178 | 205 | </div> | |
| 179 | 206 | <div> | |
| 180 | 207 | <dt> | |
| @@ -194,19 +221,21 @@ | |||
| 194 | 221 | <dt> | |
| 195 | 222 | <kbd>esc</kbd> | |
| 196 | 223 | </dt> | |
| 197 | - | <dd>close panel</dd> | |
| 224 | + | <dd>close a dialog, leave a field</dd> | |
| 198 | 225 | </div> | |
| 199 | 226 | </dl> | |
| 200 | 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 | 229 | <div class="panel-head"> | |
| 203 | - | <label for="refs-filter">ref></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))"> | |
| 205 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 230 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 232 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 233 | + | <kbd>esc</kbd> | |
| 234 | + | </button> | |
| 206 | 235 | </div> | |
| 207 | 236 | <div class="panel-cols" data-picklist> | |
| 208 | 237 | <section aria-label="Branches"> | |
| 209 | - | <h2>branches <span class="mute">2</span> | |
| 238 | + | <h2>Branches <span class="count">2</span> | |
| 210 | 239 | </h2> | |
| 211 | 240 | <ul> | |
| 212 | 241 | <li> | |
| @@ -220,7 +249,7 @@ | |||
| 220 | 249 | </ul> | |
| 221 | 250 | </section> | |
| 222 | 251 | <section aria-label="Tags"> | |
| 223 | - | <h2>tags <span class="mute">1</span> | |
| 252 | + | <h2>Tags <span class="count">1</span> | |
| 224 | 253 | </h2> | |
| 225 | 254 | <ul> | |
| 226 | 255 | <li> | |
| @@ -231,105 +260,124 @@ | |||
| 231 | 260 | </section> | |
| 232 | 261 | </div> | |
| 233 | 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 | 264 | <div class="panel-head"> | |
| 236 | - | <label for="cmd-q" class="hi">:</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(); | |
| 265 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 267 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 239 | 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 | 269 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 241 | 270 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 242 | 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 | 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 | 276 | </div> | |
| 246 | - | <ul id="cmd-list" class="cmd-list"> | |
| 277 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 247 | 278 | <li data-k="go home" data-cursor> | |
| 248 | 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 | 283 | </li> | |
| 251 | 284 | <li data-k="section summary"> | |
| 252 | 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 | 289 | </li> | |
| 255 | - | <li data-k="section tree"> | |
| 290 | + | <li data-k="section files"> | |
| 256 | 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 | 295 | </li> | |
| 259 | 296 | <li data-k="section commits"> | |
| 260 | 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 | 301 | </li> | |
| 263 | - | <li data-k="section refs"> | |
| 302 | + | <li data-k="section branches & tags"> | |
| 264 | 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 | 307 | </li> | |
| 267 | 308 | <li data-k="section merge requests"> | |
| 268 | 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 | 313 | </li> | |
| 271 | 314 | <li data-k="branch feature/x"> | |
| 272 | 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 | 319 | </li> | |
| 275 | 320 | <li data-k="branch main"> | |
| 276 | 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 | 325 | </li> | |
| 279 | 326 | <li data-k="tag v1.0"> | |
| 280 | 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 | 331 | </li> | |
| 283 | 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 | 342 | </dialog> | |
| 285 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 343 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 300 | 344 | if (q('dialog[open]').count) return; | |
| 301 | 345 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 302 | 346 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 303 | 347 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 304 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 305 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 348 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 352 | })(1); | |
| 307 | - | k-k from:body -> ((d) => { | |
| 353 | + | k-k from:body -> (async (d) => { | |
| 308 | 354 | if (q('dialog[open]').count) return; | |
| 309 | 355 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 310 | 356 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 311 | 357 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 312 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 313 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 358 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 362 | })(-1); | |
| 315 | 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 | 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 | 365 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 318 | 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(); | |
| 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()); | |
| 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(); | |
| 367 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 370 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 323 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 324 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 325 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 326 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 327 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 328 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 329 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 330 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 331 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 332 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 371 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 372 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 373 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 374 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 375 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 376 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 377 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 378 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 379 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 380 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 333 | 381 | k-next from:body -> { | |
| 334 | 382 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 335 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>main.go · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,36 +42,55 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <h1 class="file-h"> | |
| 66 | 89 | <span>main.go</span> | |
| 67 | 90 | <span class="acts"> | |
| 68 | - | <a href="/files/raw/main/main.go">raw</a> | |
| 69 | - | <a href="/files/commits/main/main.go">history</a> | |
| 70 | - | <a href="/files/tree/main">dir</a> | |
| 91 | + | <a class="btn" href="/files/raw/main/main.go">Raw</a> | |
| 92 | + | <a class="btn" href="/files/commits/main/main.go">History</a> | |
| 93 | + | <a class="btn" href="/files/tree/main">Folder</a> | |
| 71 | 94 | </span> | |
| 72 | 95 | </h1> | |
| 73 | 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 | 159 | </pre> | |
| 137 | 160 | </main> | |
| 138 | 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 | 162 | <section aria-labelledby="r-file"> | |
| 142 | - | <h2 id="r-file">file</h2> | |
| 163 | + | <h2 id="r-file">File</h2> | |
| 143 | 164 | <dl class="kv"> | |
| 144 | 165 | <dt>size</dt> | |
| 145 | 166 | <dd>87 B</dd> | |
| @@ -150,7 +171,7 @@ | |||
| 150 | 171 | </dl> | |
| 151 | 172 | </section> | |
| 152 | 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 | 175 | </h2> | |
| 155 | 176 | <p> | |
| 156 | 177 | <a href="/files/commit/SHA40">SHA7</a> add main.go</p> | |
| @@ -158,77 +179,77 @@ | |||
| 158 | 179 | </p> | |
| 159 | 180 | </section> | |
| 160 | 181 | <section aria-labelledby="r-sel"> | |
| 161 | - | <h2 id="r-sel">selection</h2> | |
| 182 | + | <h2 id="r-sel">Selection</h2> | |
| 162 | 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 | 184 | <p class="dim">y copies a permalink pinned to the commit sha</p> | |
| 164 | 185 | </section> | |
| 165 | - | </details> | |
| 166 | 186 | </aside> | |
| 167 | 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 | 203 | <div class="panel-head"> | |
| 170 | - | <span>keys</span> | |
| 171 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 204 | + | <h2>Keyboard shortcuts</h2> | |
| 205 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 206 | + | <kbd>esc</kbd> close</button> | |
| 172 | 207 | </div> | |
| 173 | 208 | <dl class="keys"> | |
| 174 | 209 | <div> | |
| 175 | 210 | <dt> | |
| 176 | - | <kbd>j</kbd> | |
| 177 | - | <kbd>k</kbd> | |
| 211 | + | <kbd>ctrl k</kbd> | |
| 212 | + | <kbd>:</kbd> | |
| 178 | 213 | </dt> | |
| 179 | - | <dd>move cursor down / up</dd> | |
| 214 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 180 | 215 | </div> | |
| 181 | 216 | <div> | |
| 182 | 217 | <dt> | |
| 183 | - | <kbd>enter</kbd> | |
| 218 | + | <kbd>?</kbd> | |
| 184 | 219 | </dt> | |
| 185 | - | <dd>open row under cursor</dd> | |
| 220 | + | <dd>this list</dd> | |
| 186 | 221 | </div> | |
| 187 | 222 | <div> | |
| 188 | 223 | <dt> | |
| 189 | - | <kbd>g</kbd> | |
| 190 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 224 | + | <kbd>j</kbd> | |
| 225 | + | <kbd>k</kbd> | |
| 191 | 226 | </dt> | |
| 192 | - | <dd>first / last row</dd> | |
| 227 | + | <dd>move the row cursor down / up</dd> | |
| 193 | 228 | </div> | |
| 194 | 229 | <div> | |
| 195 | 230 | <dt> | |
| 196 | - | <kbd>/</kbd> | |
| 231 | + | <kbd>enter</kbd> | |
| 197 | 232 | </dt> | |
| 198 | - | <dd>focus the filter</dd> | |
| 233 | + | <dd>open the row under the cursor</dd> | |
| 199 | 234 | </div> | |
| 200 | 235 | <div> | |
| 201 | 236 | <dt> | |
| 202 | - | <kbd>g h</kbd> | |
| 203 | - | <kbd>g t</kbd> | |
| 237 | + | <kbd>g g</kbd> | |
| 238 | + | <kbd>G</kbd> | |
| 204 | 239 | </dt> | |
| 205 | - | <dd>go home / tree</dd> | |
| 240 | + | <dd>first / last row</dd> | |
| 206 | 241 | </div> | |
| 207 | 242 | <div> | |
| 208 | 243 | <dt> | |
| 209 | - | <kbd>g c</kbd> | |
| 210 | - | <kbd>g r</kbd> | |
| 244 | + | <kbd>/</kbd> | |
| 211 | 245 | </dt> | |
| 212 | - | <dd>go commits / refs</dd> | |
| 246 | + | <dd>filter the list</dd> | |
| 213 | 247 | </div> | |
| 214 | 248 | <div> | |
| 215 | 249 | <dt> | |
| 216 | 250 | <kbd>b</kbd> | |
| 217 | 251 | </dt> | |
| 218 | - | <dd>switch ref (branches, tags)</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> | |
| 252 | + | <dd>switch branch or tag</dd> | |
| 232 | 253 | </div> | |
| 233 | 254 | <div> | |
| 234 | 255 | <dt> | |
| @@ -236,15 +257,21 @@ | |||
| 236 | 257 | <kbd>t</kbd> | |
| 237 | 258 | <kbd>c</kbd> | |
| 238 | 259 | <kbd>r</kbd> | |
| 260 | + | <kbd>m</kbd> | |
| 239 | 261 | </dt> | |
| 240 | - | <dd>repo sections (when shown)</dd> | |
| 262 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 241 | 263 | </div> | |
| 242 | 264 | <div> | |
| 243 | 265 | <dt> | |
| 244 | - | <kbd>m</kbd> | |
| 245 | - | <kbd>g m</kbd> | |
| 266 | + | <kbd>g h</kbd> | |
| 267 | + | </dt> | |
| 268 | + | <dd>home</dd> | |
| 269 | + | </div> | |
| 270 | + | <div> | |
| 271 | + | <dt> | |
| 272 | + | <kbd>y</kbd> | |
| 246 | 273 | </dt> | |
| 247 | - | <dd>merge requests (when shown)</dd> | |
| 274 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 248 | 275 | </div> | |
| 249 | 276 | <div> | |
| 250 | 277 | <dt> | |
| @@ -264,19 +291,21 @@ | |||
| 264 | 291 | <dt> | |
| 265 | 292 | <kbd>esc</kbd> | |
| 266 | 293 | </dt> | |
| 267 | - | <dd>close panel</dd> | |
| 294 | + | <dd>close a dialog, leave a field</dd> | |
| 268 | 295 | </div> | |
| 269 | 296 | </dl> | |
| 270 | 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 | 299 | <div class="panel-head"> | |
| 273 | - | <label for="refs-filter">ref></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))"> | |
| 275 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 300 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 302 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 303 | + | <kbd>esc</kbd> | |
| 304 | + | </button> | |
| 276 | 305 | </div> | |
| 277 | 306 | <div class="panel-cols" data-picklist> | |
| 278 | 307 | <section aria-label="Branches"> | |
| 279 | - | <h2>branches <span class="mute">2</span> | |
| 308 | + | <h2>Branches <span class="count">2</span> | |
| 280 | 309 | </h2> | |
| 281 | 310 | <ul> | |
| 282 | 311 | <li> | |
| @@ -290,7 +319,7 @@ | |||
| 290 | 319 | </ul> | |
| 291 | 320 | </section> | |
| 292 | 321 | <section aria-label="Tags"> | |
| 293 | - | <h2>tags <span class="mute">1</span> | |
| 322 | + | <h2>Tags <span class="count">1</span> | |
| 294 | 323 | </h2> | |
| 295 | 324 | <ul> | |
| 296 | 325 | <li> | |
| @@ -301,106 +330,124 @@ | |||
| 301 | 330 | </section> | |
| 302 | 331 | </div> | |
| 303 | 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 | 334 | <div class="panel-head"> | |
| 306 | - | <label for="cmd-q" class="hi">:</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(); | |
| 335 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 337 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 309 | 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 | 339 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 311 | 340 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 312 | 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 | 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 | 346 | </div> | |
| 316 | - | <ul id="cmd-list" class="cmd-list"> | |
| 347 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 317 | 348 | <li data-k="go home" data-cursor> | |
| 318 | 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 | 353 | </li> | |
| 321 | 354 | <li data-k="section summary"> | |
| 322 | 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 | 359 | </li> | |
| 325 | - | <li data-k="section tree"> | |
| 360 | + | <li data-k="section files"> | |
| 326 | 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 | 365 | </li> | |
| 329 | 366 | <li data-k="section commits"> | |
| 330 | 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 | 371 | </li> | |
| 333 | - | <li data-k="section refs"> | |
| 372 | + | <li data-k="section branches & tags"> | |
| 334 | 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 | 377 | </li> | |
| 337 | 378 | <li data-k="section merge requests"> | |
| 338 | 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 | 383 | </li> | |
| 341 | 384 | <li data-k="branch feature/x"> | |
| 342 | 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 | 389 | </li> | |
| 345 | 390 | <li data-k="branch main"> | |
| 346 | 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 | 395 | </li> | |
| 349 | 396 | <li data-k="tag v1.0"> | |
| 350 | 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 | 401 | </li> | |
| 353 | 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 | 412 | </dialog> | |
| 355 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 413 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 371 | 414 | if (q('dialog[open]').count) return; | |
| 372 | 415 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 373 | 416 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 374 | 417 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 375 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 376 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 418 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 422 | })(1); | |
| 378 | - | k-k from:body -> ((d) => { | |
| 423 | + | k-k from:body -> (async (d) => { | |
| 379 | 424 | if (q('dialog[open]').count) return; | |
| 380 | 425 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 381 | 426 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 382 | 427 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 383 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 384 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 428 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 432 | })(-1); | |
| 386 | 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 | 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 | 435 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 389 | 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(); | |
| 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()); | |
| 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(); | |
| 437 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 440 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 394 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 395 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 396 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 397 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 398 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 399 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 400 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 401 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 402 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 403 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 441 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 442 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 443 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 444 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 445 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 446 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 447 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 448 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 449 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 450 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 404 | 451 | k-next from:body -> { | |
| 405 | 452 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 406 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>empty.txt · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,45 +42,62 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <h1 class="file-h"> | |
| 66 | 89 | <span>empty.txt</span> | |
| 67 | 90 | <span class="acts"> | |
| 68 | - | <a href="/files/raw/main/empty.txt">raw</a> | |
| 69 | - | <a href="/files/commits/main/empty.txt">history</a> | |
| 70 | - | <a href="/files/tree/main">dir</a> | |
| 91 | + | <a class="btn" href="/files/raw/main/empty.txt">Raw</a> | |
| 92 | + | <a class="btn" href="/files/commits/main/empty.txt">History</a> | |
| 93 | + | <a class="btn" href="/files/tree/main">Folder</a> | |
| 71 | 94 | </span> | |
| 72 | 95 | </h1> | |
| 73 | 96 | <p class="notice" role="note">empty file.</p> | |
| 74 | 97 | </main> | |
| 75 | 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 | 99 | <section aria-labelledby="r-file"> | |
| 79 | - | <h2 id="r-file">file</h2> | |
| 100 | + | <h2 id="r-file">File</h2> | |
| 80 | 101 | <dl class="kv"> | |
| 81 | 102 | <dt>size</dt> | |
| 82 | 103 | <dd>0 B</dd> | |
| @@ -85,80 +106,80 @@ | |||
| 85 | 106 | </dl> | |
| 86 | 107 | </section> | |
| 87 | 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 | 110 | </h2> | |
| 90 | 111 | <p> | |
| 91 | 112 | <a href="/files/commit/SHA40">SHA7</a> add empty.txt</p> | |
| 92 | 113 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | |
| 93 | 114 | </p> | |
| 94 | 115 | </section> | |
| 95 | - | </details> | |
| 96 | 116 | </aside> | |
| 97 | 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 | 133 | <div class="panel-head"> | |
| 100 | - | <span>keys</span> | |
| 101 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 134 | + | <h2>Keyboard shortcuts</h2> | |
| 135 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 136 | + | <kbd>esc</kbd> close</button> | |
| 102 | 137 | </div> | |
| 103 | 138 | <dl class="keys"> | |
| 104 | 139 | <div> | |
| 105 | 140 | <dt> | |
| 106 | - | <kbd>j</kbd> | |
| 107 | - | <kbd>k</kbd> | |
| 141 | + | <kbd>ctrl k</kbd> | |
| 142 | + | <kbd>:</kbd> | |
| 108 | 143 | </dt> | |
| 109 | - | <dd>move cursor down / up</dd> | |
| 144 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 110 | 145 | </div> | |
| 111 | 146 | <div> | |
| 112 | 147 | <dt> | |
| 113 | - | <kbd>enter</kbd> | |
| 148 | + | <kbd>?</kbd> | |
| 114 | 149 | </dt> | |
| 115 | - | <dd>open row under cursor</dd> | |
| 150 | + | <dd>this list</dd> | |
| 116 | 151 | </div> | |
| 117 | 152 | <div> | |
| 118 | 153 | <dt> | |
| 119 | - | <kbd>g</kbd> | |
| 120 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 154 | + | <kbd>j</kbd> | |
| 155 | + | <kbd>k</kbd> | |
| 121 | 156 | </dt> | |
| 122 | - | <dd>first / last row</dd> | |
| 157 | + | <dd>move the row cursor down / up</dd> | |
| 123 | 158 | </div> | |
| 124 | 159 | <div> | |
| 125 | 160 | <dt> | |
| 126 | - | <kbd>/</kbd> | |
| 161 | + | <kbd>enter</kbd> | |
| 127 | 162 | </dt> | |
| 128 | - | <dd>focus the filter</dd> | |
| 163 | + | <dd>open the row under the cursor</dd> | |
| 129 | 164 | </div> | |
| 130 | 165 | <div> | |
| 131 | 166 | <dt> | |
| 132 | - | <kbd>g h</kbd> | |
| 133 | - | <kbd>g t</kbd> | |
| 167 | + | <kbd>g g</kbd> | |
| 168 | + | <kbd>G</kbd> | |
| 134 | 169 | </dt> | |
| 135 | - | <dd>go home / tree</dd> | |
| 170 | + | <dd>first / last row</dd> | |
| 136 | 171 | </div> | |
| 137 | 172 | <div> | |
| 138 | 173 | <dt> | |
| 139 | - | <kbd>g c</kbd> | |
| 140 | - | <kbd>g r</kbd> | |
| 174 | + | <kbd>/</kbd> | |
| 141 | 175 | </dt> | |
| 142 | - | <dd>go commits / refs</dd> | |
| 176 | + | <dd>filter the list</dd> | |
| 143 | 177 | </div> | |
| 144 | 178 | <div> | |
| 145 | 179 | <dt> | |
| 146 | 180 | <kbd>b</kbd> | |
| 147 | 181 | </dt> | |
| 148 | - | <dd>switch ref (branches, tags)</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> | |
| 182 | + | <dd>switch branch or tag</dd> | |
| 162 | 183 | </div> | |
| 163 | 184 | <div> | |
| 164 | 185 | <dt> | |
| @@ -166,15 +187,21 @@ | |||
| 166 | 187 | <kbd>t</kbd> | |
| 167 | 188 | <kbd>c</kbd> | |
| 168 | 189 | <kbd>r</kbd> | |
| 190 | + | <kbd>m</kbd> | |
| 169 | 191 | </dt> | |
| 170 | - | <dd>repo sections (when shown)</dd> | |
| 192 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 171 | 193 | </div> | |
| 172 | 194 | <div> | |
| 173 | 195 | <dt> | |
| 174 | - | <kbd>m</kbd> | |
| 175 | - | <kbd>g m</kbd> | |
| 196 | + | <kbd>g h</kbd> | |
| 197 | + | </dt> | |
| 198 | + | <dd>home</dd> | |
| 199 | + | </div> | |
| 200 | + | <div> | |
| 201 | + | <dt> | |
| 202 | + | <kbd>y</kbd> | |
| 176 | 203 | </dt> | |
| 177 | - | <dd>merge requests (when shown)</dd> | |
| 204 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 178 | 205 | </div> | |
| 179 | 206 | <div> | |
| 180 | 207 | <dt> | |
| @@ -194,19 +221,21 @@ | |||
| 194 | 221 | <dt> | |
| 195 | 222 | <kbd>esc</kbd> | |
| 196 | 223 | </dt> | |
| 197 | - | <dd>close panel</dd> | |
| 224 | + | <dd>close a dialog, leave a field</dd> | |
| 198 | 225 | </div> | |
| 199 | 226 | </dl> | |
| 200 | 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 | 229 | <div class="panel-head"> | |
| 203 | - | <label for="refs-filter">ref></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))"> | |
| 205 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 230 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 232 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 233 | + | <kbd>esc</kbd> | |
| 234 | + | </button> | |
| 206 | 235 | </div> | |
| 207 | 236 | <div class="panel-cols" data-picklist> | |
| 208 | 237 | <section aria-label="Branches"> | |
| 209 | - | <h2>branches <span class="mute">2</span> | |
| 238 | + | <h2>Branches <span class="count">2</span> | |
| 210 | 239 | </h2> | |
| 211 | 240 | <ul> | |
| 212 | 241 | <li> | |
| @@ -220,7 +249,7 @@ | |||
| 220 | 249 | </ul> | |
| 221 | 250 | </section> | |
| 222 | 251 | <section aria-label="Tags"> | |
| 223 | - | <h2>tags <span class="mute">1</span> | |
| 252 | + | <h2>Tags <span class="count">1</span> | |
| 224 | 253 | </h2> | |
| 225 | 254 | <ul> | |
| 226 | 255 | <li> | |
| @@ -231,105 +260,124 @@ | |||
| 231 | 260 | </section> | |
| 232 | 261 | </div> | |
| 233 | 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 | 264 | <div class="panel-head"> | |
| 236 | - | <label for="cmd-q" class="hi">:</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(); | |
| 265 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 267 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 239 | 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 | 269 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 241 | 270 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 242 | 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 | 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 | 276 | </div> | |
| 246 | - | <ul id="cmd-list" class="cmd-list"> | |
| 277 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 247 | 278 | <li data-k="go home" data-cursor> | |
| 248 | 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 | 283 | </li> | |
| 251 | 284 | <li data-k="section summary"> | |
| 252 | 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 | 289 | </li> | |
| 255 | - | <li data-k="section tree"> | |
| 290 | + | <li data-k="section files"> | |
| 256 | 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 | 295 | </li> | |
| 259 | 296 | <li data-k="section commits"> | |
| 260 | 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 | 301 | </li> | |
| 263 | - | <li data-k="section refs"> | |
| 302 | + | <li data-k="section branches & tags"> | |
| 264 | 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 | 307 | </li> | |
| 267 | 308 | <li data-k="section merge requests"> | |
| 268 | 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 | 313 | </li> | |
| 271 | 314 | <li data-k="branch feature/x"> | |
| 272 | 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 | 319 | </li> | |
| 275 | 320 | <li data-k="branch main"> | |
| 276 | 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 | 325 | </li> | |
| 279 | 326 | <li data-k="tag v1.0"> | |
| 280 | 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 | 331 | </li> | |
| 283 | 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 | 342 | </dialog> | |
| 285 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 343 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 300 | 344 | if (q('dialog[open]').count) return; | |
| 301 | 345 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 302 | 346 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 303 | 347 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 304 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 305 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 348 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 352 | })(1); | |
| 307 | - | k-k from:body -> ((d) => { | |
| 353 | + | k-k from:body -> (async (d) => { | |
| 308 | 354 | if (q('dialog[open]').count) return; | |
| 309 | 355 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 310 | 356 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 311 | 357 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 312 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 313 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 358 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 362 | })(-1); | |
| 315 | 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 | 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 | 365 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 318 | 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(); | |
| 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()); | |
| 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(); | |
| 367 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 370 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 323 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 324 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 325 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 326 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 327 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 328 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 329 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 330 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 331 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 332 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 371 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 372 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 373 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 374 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 375 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 376 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 377 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 378 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 379 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 380 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 333 | 381 | k-next from:body -> { | |
| 334 | 382 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 335 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>pic.png · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,36 +42,55 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <h1 class="file-h"> | |
| 66 | 89 | <span>pic.png</span> | |
| 67 | 90 | <span class="acts"> | |
| 68 | - | <a href="/files/raw/main/pic.png">raw</a> | |
| 69 | - | <a href="/files/commits/main/pic.png">history</a> | |
| 70 | - | <a href="/files/tree/main">dir</a> | |
| 91 | + | <a class="btn" href="/files/raw/main/pic.png">Raw</a> | |
| 92 | + | <a class="btn" href="/files/commits/main/pic.png">History</a> | |
| 93 | + | <a class="btn" href="/files/tree/main">Folder</a> | |
| 71 | 94 | </span> | |
| 72 | 95 | </h1> | |
| 73 | 96 | <p class="img-view"> | |
| @@ -75,10 +98,8 @@ | |||
| 75 | 98 | </p> | |
| 76 | 99 | </main> | |
| 77 | 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 | 101 | <section aria-labelledby="r-file"> | |
| 81 | - | <h2 id="r-file">file</h2> | |
| 102 | + | <h2 id="r-file">File</h2> | |
| 82 | 103 | <dl class="kv"> | |
| 83 | 104 | <dt>size</dt> | |
| 84 | 105 | <dd>88 B</dd> | |
| @@ -87,80 +108,80 @@ | |||
| 87 | 108 | </dl> | |
| 88 | 109 | </section> | |
| 89 | 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 | 112 | </h2> | |
| 92 | 113 | <p> | |
| 93 | 114 | <a href="/files/commit/SHA40">SHA7</a> add pic.png</p> | |
| 94 | 115 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | |
| 95 | 116 | </p> | |
| 96 | 117 | </section> | |
| 97 | - | </details> | |
| 98 | 118 | </aside> | |
| 99 | 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 | 135 | <div class="panel-head"> | |
| 102 | - | <span>keys</span> | |
| 103 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 136 | + | <h2>Keyboard shortcuts</h2> | |
| 137 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 138 | + | <kbd>esc</kbd> close</button> | |
| 104 | 139 | </div> | |
| 105 | 140 | <dl class="keys"> | |
| 106 | 141 | <div> | |
| 107 | 142 | <dt> | |
| 108 | - | <kbd>j</kbd> | |
| 109 | - | <kbd>k</kbd> | |
| 143 | + | <kbd>ctrl k</kbd> | |
| 144 | + | <kbd>:</kbd> | |
| 110 | 145 | </dt> | |
| 111 | - | <dd>move cursor down / up</dd> | |
| 146 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 112 | 147 | </div> | |
| 113 | 148 | <div> | |
| 114 | 149 | <dt> | |
| 115 | - | <kbd>enter</kbd> | |
| 150 | + | <kbd>?</kbd> | |
| 116 | 151 | </dt> | |
| 117 | - | <dd>open row under cursor</dd> | |
| 152 | + | <dd>this list</dd> | |
| 118 | 153 | </div> | |
| 119 | 154 | <div> | |
| 120 | 155 | <dt> | |
| 121 | - | <kbd>g</kbd> | |
| 122 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 156 | + | <kbd>j</kbd> | |
| 157 | + | <kbd>k</kbd> | |
| 123 | 158 | </dt> | |
| 124 | - | <dd>first / last row</dd> | |
| 159 | + | <dd>move the row cursor down / up</dd> | |
| 125 | 160 | </div> | |
| 126 | 161 | <div> | |
| 127 | 162 | <dt> | |
| 128 | - | <kbd>/</kbd> | |
| 163 | + | <kbd>enter</kbd> | |
| 129 | 164 | </dt> | |
| 130 | - | <dd>focus the filter</dd> | |
| 165 | + | <dd>open the row under the cursor</dd> | |
| 131 | 166 | </div> | |
| 132 | 167 | <div> | |
| 133 | 168 | <dt> | |
| 134 | - | <kbd>g h</kbd> | |
| 135 | - | <kbd>g t</kbd> | |
| 169 | + | <kbd>g g</kbd> | |
| 170 | + | <kbd>G</kbd> | |
| 136 | 171 | </dt> | |
| 137 | - | <dd>go home / tree</dd> | |
| 172 | + | <dd>first / last row</dd> | |
| 138 | 173 | </div> | |
| 139 | 174 | <div> | |
| 140 | 175 | <dt> | |
| 141 | - | <kbd>g c</kbd> | |
| 142 | - | <kbd>g r</kbd> | |
| 176 | + | <kbd>/</kbd> | |
| 143 | 177 | </dt> | |
| 144 | - | <dd>go commits / refs</dd> | |
| 178 | + | <dd>filter the list</dd> | |
| 145 | 179 | </div> | |
| 146 | 180 | <div> | |
| 147 | 181 | <dt> | |
| 148 | 182 | <kbd>b</kbd> | |
| 149 | 183 | </dt> | |
| 150 | - | <dd>switch ref (branches, tags)</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> | |
| 184 | + | <dd>switch branch or tag</dd> | |
| 164 | 185 | </div> | |
| 165 | 186 | <div> | |
| 166 | 187 | <dt> | |
| @@ -168,15 +189,21 @@ | |||
| 168 | 189 | <kbd>t</kbd> | |
| 169 | 190 | <kbd>c</kbd> | |
| 170 | 191 | <kbd>r</kbd> | |
| 192 | + | <kbd>m</kbd> | |
| 171 | 193 | </dt> | |
| 172 | - | <dd>repo sections (when shown)</dd> | |
| 194 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 173 | 195 | </div> | |
| 174 | 196 | <div> | |
| 175 | 197 | <dt> | |
| 176 | - | <kbd>m</kbd> | |
| 177 | - | <kbd>g m</kbd> | |
| 198 | + | <kbd>g h</kbd> | |
| 199 | + | </dt> | |
| 200 | + | <dd>home</dd> | |
| 201 | + | </div> | |
| 202 | + | <div> | |
| 203 | + | <dt> | |
| 204 | + | <kbd>y</kbd> | |
| 178 | 205 | </dt> | |
| 179 | - | <dd>merge requests (when shown)</dd> | |
| 206 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 180 | 207 | </div> | |
| 181 | 208 | <div> | |
| 182 | 209 | <dt> | |
| @@ -196,19 +223,21 @@ | |||
| 196 | 223 | <dt> | |
| 197 | 224 | <kbd>esc</kbd> | |
| 198 | 225 | </dt> | |
| 199 | - | <dd>close panel</dd> | |
| 226 | + | <dd>close a dialog, leave a field</dd> | |
| 200 | 227 | </div> | |
| 201 | 228 | </dl> | |
| 202 | 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 | 231 | <div class="panel-head"> | |
| 205 | - | <label for="refs-filter">ref></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))"> | |
| 207 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 232 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 234 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 235 | + | <kbd>esc</kbd> | |
| 236 | + | </button> | |
| 208 | 237 | </div> | |
| 209 | 238 | <div class="panel-cols" data-picklist> | |
| 210 | 239 | <section aria-label="Branches"> | |
| 211 | - | <h2>branches <span class="mute">2</span> | |
| 240 | + | <h2>Branches <span class="count">2</span> | |
| 212 | 241 | </h2> | |
| 213 | 242 | <ul> | |
| 214 | 243 | <li> | |
| @@ -222,7 +251,7 @@ | |||
| 222 | 251 | </ul> | |
| 223 | 252 | </section> | |
| 224 | 253 | <section aria-label="Tags"> | |
| 225 | - | <h2>tags <span class="mute">1</span> | |
| 254 | + | <h2>Tags <span class="count">1</span> | |
| 226 | 255 | </h2> | |
| 227 | 256 | <ul> | |
| 228 | 257 | <li> | |
| @@ -233,105 +262,124 @@ | |||
| 233 | 262 | </section> | |
| 234 | 263 | </div> | |
| 235 | 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 | 266 | <div class="panel-head"> | |
| 238 | - | <label for="cmd-q" class="hi">:</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(); | |
| 267 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 269 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 241 | 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 | 271 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 243 | 272 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 244 | 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 | 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 | 278 | </div> | |
| 248 | - | <ul id="cmd-list" class="cmd-list"> | |
| 279 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 249 | 280 | <li data-k="go home" data-cursor> | |
| 250 | 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 | 285 | </li> | |
| 253 | 286 | <li data-k="section summary"> | |
| 254 | 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 | 291 | </li> | |
| 257 | - | <li data-k="section tree"> | |
| 292 | + | <li data-k="section files"> | |
| 258 | 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 | 297 | </li> | |
| 261 | 298 | <li data-k="section commits"> | |
| 262 | 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 | 303 | </li> | |
| 265 | - | <li data-k="section refs"> | |
| 304 | + | <li data-k="section branches & tags"> | |
| 266 | 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 | 309 | </li> | |
| 269 | 310 | <li data-k="section merge requests"> | |
| 270 | 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 | 315 | </li> | |
| 273 | 316 | <li data-k="branch feature/x"> | |
| 274 | 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 | 321 | </li> | |
| 277 | 322 | <li data-k="branch main"> | |
| 278 | 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 | 327 | </li> | |
| 281 | 328 | <li data-k="tag v1.0"> | |
| 282 | 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 | 333 | </li> | |
| 285 | 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 | 344 | </dialog> | |
| 287 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 345 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 302 | 346 | if (q('dialog[open]').count) return; | |
| 303 | 347 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 304 | 348 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 305 | 349 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 306 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 307 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 350 | + | 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' }); | |
| 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 | 354 | })(1); | |
| 309 | - | k-k from:body -> ((d) => { | |
| 355 | + | k-k from:body -> (async (d) => { | |
| 310 | 356 | if (q('dialog[open]').count) return; | |
| 311 | 357 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 312 | 358 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 313 | 359 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 314 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 315 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 360 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 364 | })(-1); | |
| 317 | 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 | 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 | 367 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 320 | 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(); | |
| 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()); | |
| 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(); | |
| 369 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 372 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 325 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 326 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 327 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 328 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 329 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 330 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 331 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 332 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 333 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 334 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 373 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 374 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 375 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 376 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 377 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 378 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 379 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 380 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 381 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 382 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 335 | 383 | k-next from:body -> { | |
| 336 | 384 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 337 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>docs/guide.md · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -41,36 +45,55 @@ | |||
| 41 | 45 | </li> | |
| 42 | 46 | </ol> | |
| 43 | 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 | 65 | <ul> | |
| 46 | 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 | 69 | </li> | |
| 49 | 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 | 73 | </li> | |
| 52 | 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 | 77 | </li> | |
| 55 | 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 | 81 | </li> | |
| 58 | 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 | 85 | </li> | |
| 61 | 86 | </ul> | |
| 62 | 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 | 88 | </header> | |
| 66 | - | <div class="work has-rail read"> | |
| 67 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | |
| 89 | + | <div class="work has-rail"> | |
| 90 | + | <main id="main" tabindex="-1"> | |
| 68 | 91 | <h1 class="file-h"> | |
| 69 | 92 | <span>guide.md</span> | |
| 70 | 93 | <span class="acts"> | |
| 71 | - | <a href="/files/raw/main/docs/guide.md">raw</a> | |
| 72 | - | <a href="/files/commits/main/docs/guide.md">history</a> | |
| 73 | - | <a href="/files/tree/main/docs">dir</a> | |
| 94 | + | <a class="btn" href="/files/raw/main/docs/guide.md">Raw</a> | |
| 95 | + | <a class="btn" href="/files/commits/main/docs/guide.md">History</a> | |
| 96 | + | <a class="btn" href="/files/tree/main/docs">Folder</a> | |
| 74 | 97 | </span> | |
| 75 | 98 | </h1> | |
| 76 | 99 | <article class="prose" aria-label="guide.md"> | |
| @@ -126,10 +149,8 @@ | |||
| 126 | 149 | </details> | |
| 127 | 150 | </main> | |
| 128 | 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 | 152 | <section aria-labelledby="r-file"> | |
| 132 | - | <h2 id="r-file">file</h2> | |
| 153 | + | <h2 id="r-file">File</h2> | |
| 133 | 154 | <dl class="kv"> | |
| 134 | 155 | <dt>size</dt> | |
| 135 | 156 | <dd>47 B</dd> | |
| @@ -140,7 +161,7 @@ | |||
| 140 | 161 | </dl> | |
| 141 | 162 | </section> | |
| 142 | 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 | 165 | </h2> | |
| 145 | 166 | <p> | |
| 146 | 167 | <a href="/files/commit/SHA40">SHA7</a> add docs/guide.md</p> | |
| @@ -148,84 +169,84 @@ | |||
| 148 | 169 | </p> | |
| 149 | 170 | </section> | |
| 150 | 171 | <section aria-labelledby="r-outline"> | |
| 151 | - | <h2 id="r-outline">outline</h2> | |
| 172 | + | <h2 id="r-outline">On this page</h2> | |
| 152 | 173 | <nav class="outline" aria-label="Outline"> | |
| 153 | 174 | <a class="l1" href="#md-guide">Guide</a> | |
| 154 | 175 | <a class="l2" href="#md-steps">Steps</a> | |
| 155 | 176 | </nav> | |
| 156 | 177 | </section> | |
| 157 | 178 | <section aria-labelledby="r-sel"> | |
| 158 | - | <h2 id="r-sel">selection</h2> | |
| 179 | + | <h2 id="r-sel">Selection</h2> | |
| 159 | 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 | 181 | <p class="dim">y copies a permalink pinned to the commit sha</p> | |
| 161 | 182 | </section> | |
| 162 | - | </details> | |
| 163 | 183 | </aside> | |
| 164 | 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 | 200 | <div class="panel-head"> | |
| 167 | - | <span>keys</span> | |
| 168 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 201 | + | <h2>Keyboard shortcuts</h2> | |
| 202 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 203 | + | <kbd>esc</kbd> close</button> | |
| 169 | 204 | </div> | |
| 170 | 205 | <dl class="keys"> | |
| 171 | 206 | <div> | |
| 172 | 207 | <dt> | |
| 173 | - | <kbd>j</kbd> | |
| 174 | - | <kbd>k</kbd> | |
| 208 | + | <kbd>ctrl k</kbd> | |
| 209 | + | <kbd>:</kbd> | |
| 175 | 210 | </dt> | |
| 176 | - | <dd>move cursor down / up</dd> | |
| 211 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 177 | 212 | </div> | |
| 178 | 213 | <div> | |
| 179 | 214 | <dt> | |
| 180 | - | <kbd>enter</kbd> | |
| 215 | + | <kbd>?</kbd> | |
| 181 | 216 | </dt> | |
| 182 | - | <dd>open row under cursor</dd> | |
| 217 | + | <dd>this list</dd> | |
| 183 | 218 | </div> | |
| 184 | 219 | <div> | |
| 185 | 220 | <dt> | |
| 186 | - | <kbd>g</kbd> | |
| 187 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 221 | + | <kbd>j</kbd> | |
| 222 | + | <kbd>k</kbd> | |
| 188 | 223 | </dt> | |
| 189 | - | <dd>first / last row</dd> | |
| 224 | + | <dd>move the row cursor down / up</dd> | |
| 190 | 225 | </div> | |
| 191 | 226 | <div> | |
| 192 | 227 | <dt> | |
| 193 | - | <kbd>/</kbd> | |
| 228 | + | <kbd>enter</kbd> | |
| 194 | 229 | </dt> | |
| 195 | - | <dd>focus the filter</dd> | |
| 230 | + | <dd>open the row under the cursor</dd> | |
| 196 | 231 | </div> | |
| 197 | 232 | <div> | |
| 198 | 233 | <dt> | |
| 199 | - | <kbd>g h</kbd> | |
| 200 | - | <kbd>g t</kbd> | |
| 234 | + | <kbd>g g</kbd> | |
| 235 | + | <kbd>G</kbd> | |
| 201 | 236 | </dt> | |
| 202 | - | <dd>go home / tree</dd> | |
| 237 | + | <dd>first / last row</dd> | |
| 203 | 238 | </div> | |
| 204 | 239 | <div> | |
| 205 | 240 | <dt> | |
| 206 | - | <kbd>g c</kbd> | |
| 207 | - | <kbd>g r</kbd> | |
| 241 | + | <kbd>/</kbd> | |
| 208 | 242 | </dt> | |
| 209 | - | <dd>go commits / refs</dd> | |
| 243 | + | <dd>filter the list</dd> | |
| 210 | 244 | </div> | |
| 211 | 245 | <div> | |
| 212 | 246 | <dt> | |
| 213 | 247 | <kbd>b</kbd> | |
| 214 | 248 | </dt> | |
| 215 | - | <dd>switch ref (branches, tags)</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> | |
| 249 | + | <dd>switch branch or tag</dd> | |
| 229 | 250 | </div> | |
| 230 | 251 | <div> | |
| 231 | 252 | <dt> | |
| @@ -233,15 +254,21 @@ | |||
| 233 | 254 | <kbd>t</kbd> | |
| 234 | 255 | <kbd>c</kbd> | |
| 235 | 256 | <kbd>r</kbd> | |
| 257 | + | <kbd>m</kbd> | |
| 236 | 258 | </dt> | |
| 237 | - | <dd>repo sections (when shown)</dd> | |
| 259 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 238 | 260 | </div> | |
| 239 | 261 | <div> | |
| 240 | 262 | <dt> | |
| 241 | - | <kbd>m</kbd> | |
| 242 | - | <kbd>g m</kbd> | |
| 263 | + | <kbd>g h</kbd> | |
| 264 | + | </dt> | |
| 265 | + | <dd>home</dd> | |
| 266 | + | </div> | |
| 267 | + | <div> | |
| 268 | + | <dt> | |
| 269 | + | <kbd>y</kbd> | |
| 243 | 270 | </dt> | |
| 244 | - | <dd>merge requests (when shown)</dd> | |
| 271 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 245 | 272 | </div> | |
| 246 | 273 | <div> | |
| 247 | 274 | <dt> | |
| @@ -261,19 +288,21 @@ | |||
| 261 | 288 | <dt> | |
| 262 | 289 | <kbd>esc</kbd> | |
| 263 | 290 | </dt> | |
| 264 | - | <dd>close panel</dd> | |
| 291 | + | <dd>close a dialog, leave a field</dd> | |
| 265 | 292 | </div> | |
| 266 | 293 | </dl> | |
| 267 | 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 | 296 | <div class="panel-head"> | |
| 270 | - | <label for="refs-filter">ref></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))"> | |
| 272 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 297 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 299 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 300 | + | <kbd>esc</kbd> | |
| 301 | + | </button> | |
| 273 | 302 | </div> | |
| 274 | 303 | <div class="panel-cols" data-picklist> | |
| 275 | 304 | <section aria-label="Branches"> | |
| 276 | - | <h2>branches <span class="mute">2</span> | |
| 305 | + | <h2>Branches <span class="count">2</span> | |
| 277 | 306 | </h2> | |
| 278 | 307 | <ul> | |
| 279 | 308 | <li> | |
| @@ -287,7 +316,7 @@ | |||
| 287 | 316 | </ul> | |
| 288 | 317 | </section> | |
| 289 | 318 | <section aria-label="Tags"> | |
| 290 | - | <h2>tags <span class="mute">1</span> | |
| 319 | + | <h2>Tags <span class="count">1</span> | |
| 291 | 320 | </h2> | |
| 292 | 321 | <ul> | |
| 293 | 322 | <li> | |
| @@ -298,106 +327,124 @@ | |||
| 298 | 327 | </section> | |
| 299 | 328 | </div> | |
| 300 | 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 | 331 | <div class="panel-head"> | |
| 303 | - | <label for="cmd-q" class="hi">:</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(); | |
| 332 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 334 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 306 | 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 | 336 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 308 | 337 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 309 | 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 | 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 | 343 | </div> | |
| 313 | - | <ul id="cmd-list" class="cmd-list"> | |
| 344 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 314 | 345 | <li data-k="go home" data-cursor> | |
| 315 | 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 | 350 | </li> | |
| 318 | 351 | <li data-k="section summary"> | |
| 319 | 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 | 356 | </li> | |
| 322 | - | <li data-k="section tree"> | |
| 357 | + | <li data-k="section files"> | |
| 323 | 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 | 362 | </li> | |
| 326 | 363 | <li data-k="section commits"> | |
| 327 | 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 | 368 | </li> | |
| 330 | - | <li data-k="section refs"> | |
| 369 | + | <li data-k="section branches & tags"> | |
| 331 | 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 | 374 | </li> | |
| 334 | 375 | <li data-k="section merge requests"> | |
| 335 | 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 | 380 | </li> | |
| 338 | 381 | <li data-k="branch feature/x"> | |
| 339 | 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 | 386 | </li> | |
| 342 | 387 | <li data-k="branch main"> | |
| 343 | 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 | 392 | </li> | |
| 346 | 393 | <li data-k="tag v1.0"> | |
| 347 | 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 | 398 | </li> | |
| 350 | 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 | 409 | </dialog> | |
| 352 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 410 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 368 | 411 | if (q('dialog[open]').count) return; | |
| 369 | 412 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 370 | 413 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 371 | 414 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 372 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 373 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 415 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 419 | })(1); | |
| 375 | - | k-k from:body -> ((d) => { | |
| 420 | + | k-k from:body -> (async (d) => { | |
| 376 | 421 | if (q('dialog[open]').count) return; | |
| 377 | 422 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 378 | 423 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 379 | 424 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 380 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 381 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 425 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 429 | })(-1); | |
| 383 | 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 | 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 | 432 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 386 | 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(); | |
| 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()); | |
| 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(); | |
| 434 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 437 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 391 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 392 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 393 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 394 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 395 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 396 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 397 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 398 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 399 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 400 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 438 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 439 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 440 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 441 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 442 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 443 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 444 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 445 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 446 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 447 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 401 | 448 | k-next from:body -> { | |
| 402 | 449 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 403 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>logo.svg · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,36 +42,55 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <h1 class="file-h"> | |
| 66 | 89 | <span>logo.svg</span> | |
| 67 | 90 | <span class="acts"> | |
| 68 | - | <a href="/files/raw/main/logo.svg">raw</a> | |
| 69 | - | <a href="/files/commits/main/logo.svg">history</a> | |
| 70 | - | <a href="/files/tree/main">dir</a> | |
| 91 | + | <a class="btn" href="/files/raw/main/logo.svg">Raw</a> | |
| 92 | + | <a class="btn" href="/files/commits/main/logo.svg">History</a> | |
| 93 | + | <a class="btn" href="/files/tree/main">Folder</a> | |
| 71 | 94 | </span> | |
| 72 | 95 | </h1> | |
| 73 | 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 | 114 | </pre> | |
| 92 | 115 | </main> | |
| 93 | 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 | 117 | <section aria-labelledby="r-file"> | |
| 97 | - | <h2 id="r-file">file</h2> | |
| 118 | + | <h2 id="r-file">File</h2> | |
| 98 | 119 | <dl class="kv"> | |
| 99 | 120 | <dt>size</dt> | |
| 100 | 121 | <dd>89 B</dd> | |
| @@ -105,7 +126,7 @@ | |||
| 105 | 126 | </dl> | |
| 106 | 127 | </section> | |
| 107 | 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 | 130 | </h2> | |
| 110 | 131 | <p> | |
| 111 | 132 | <a href="/files/commit/SHA40">SHA7</a> add logo.svg</p> | |
| @@ -113,77 +134,77 @@ | |||
| 113 | 134 | </p> | |
| 114 | 135 | </section> | |
| 115 | 136 | <section aria-labelledby="r-sel"> | |
| 116 | - | <h2 id="r-sel">selection</h2> | |
| 137 | + | <h2 id="r-sel">Selection</h2> | |
| 117 | 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 | 139 | <p class="dim">y copies a permalink pinned to the commit sha</p> | |
| 119 | 140 | </section> | |
| 120 | - | </details> | |
| 121 | 141 | </aside> | |
| 122 | 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 | 158 | <div class="panel-head"> | |
| 125 | - | <span>keys</span> | |
| 126 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 159 | + | <h2>Keyboard shortcuts</h2> | |
| 160 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 161 | + | <kbd>esc</kbd> close</button> | |
| 127 | 162 | </div> | |
| 128 | 163 | <dl class="keys"> | |
| 129 | 164 | <div> | |
| 130 | 165 | <dt> | |
| 131 | - | <kbd>j</kbd> | |
| 132 | - | <kbd>k</kbd> | |
| 166 | + | <kbd>ctrl k</kbd> | |
| 167 | + | <kbd>:</kbd> | |
| 133 | 168 | </dt> | |
| 134 | - | <dd>move cursor down / up</dd> | |
| 169 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 135 | 170 | </div> | |
| 136 | 171 | <div> | |
| 137 | 172 | <dt> | |
| 138 | - | <kbd>enter</kbd> | |
| 173 | + | <kbd>?</kbd> | |
| 139 | 174 | </dt> | |
| 140 | - | <dd>open row under cursor</dd> | |
| 175 | + | <dd>this list</dd> | |
| 141 | 176 | </div> | |
| 142 | 177 | <div> | |
| 143 | 178 | <dt> | |
| 144 | - | <kbd>g</kbd> | |
| 145 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 179 | + | <kbd>j</kbd> | |
| 180 | + | <kbd>k</kbd> | |
| 146 | 181 | </dt> | |
| 147 | - | <dd>first / last row</dd> | |
| 182 | + | <dd>move the row cursor down / up</dd> | |
| 148 | 183 | </div> | |
| 149 | 184 | <div> | |
| 150 | 185 | <dt> | |
| 151 | - | <kbd>/</kbd> | |
| 186 | + | <kbd>enter</kbd> | |
| 152 | 187 | </dt> | |
| 153 | - | <dd>focus the filter</dd> | |
| 188 | + | <dd>open the row under the cursor</dd> | |
| 154 | 189 | </div> | |
| 155 | 190 | <div> | |
| 156 | 191 | <dt> | |
| 157 | - | <kbd>g h</kbd> | |
| 158 | - | <kbd>g t</kbd> | |
| 192 | + | <kbd>g g</kbd> | |
| 193 | + | <kbd>G</kbd> | |
| 159 | 194 | </dt> | |
| 160 | - | <dd>go home / tree</dd> | |
| 195 | + | <dd>first / last row</dd> | |
| 161 | 196 | </div> | |
| 162 | 197 | <div> | |
| 163 | 198 | <dt> | |
| 164 | - | <kbd>g c</kbd> | |
| 165 | - | <kbd>g r</kbd> | |
| 199 | + | <kbd>/</kbd> | |
| 166 | 200 | </dt> | |
| 167 | - | <dd>go commits / refs</dd> | |
| 201 | + | <dd>filter the list</dd> | |
| 168 | 202 | </div> | |
| 169 | 203 | <div> | |
| 170 | 204 | <dt> | |
| 171 | 205 | <kbd>b</kbd> | |
| 172 | 206 | </dt> | |
| 173 | - | <dd>switch ref (branches, tags)</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> | |
| 207 | + | <dd>switch branch or tag</dd> | |
| 187 | 208 | </div> | |
| 188 | 209 | <div> | |
| 189 | 210 | <dt> | |
| @@ -191,15 +212,21 @@ | |||
| 191 | 212 | <kbd>t</kbd> | |
| 192 | 213 | <kbd>c</kbd> | |
| 193 | 214 | <kbd>r</kbd> | |
| 215 | + | <kbd>m</kbd> | |
| 194 | 216 | </dt> | |
| 195 | - | <dd>repo sections (when shown)</dd> | |
| 217 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 196 | 218 | </div> | |
| 197 | 219 | <div> | |
| 198 | 220 | <dt> | |
| 199 | - | <kbd>m</kbd> | |
| 200 | - | <kbd>g m</kbd> | |
| 221 | + | <kbd>g h</kbd> | |
| 222 | + | </dt> | |
| 223 | + | <dd>home</dd> | |
| 224 | + | </div> | |
| 225 | + | <div> | |
| 226 | + | <dt> | |
| 227 | + | <kbd>y</kbd> | |
| 201 | 228 | </dt> | |
| 202 | - | <dd>merge requests (when shown)</dd> | |
| 229 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 203 | 230 | </div> | |
| 204 | 231 | <div> | |
| 205 | 232 | <dt> | |
| @@ -219,19 +246,21 @@ | |||
| 219 | 246 | <dt> | |
| 220 | 247 | <kbd>esc</kbd> | |
| 221 | 248 | </dt> | |
| 222 | - | <dd>close panel</dd> | |
| 249 | + | <dd>close a dialog, leave a field</dd> | |
| 223 | 250 | </div> | |
| 224 | 251 | </dl> | |
| 225 | 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 | 254 | <div class="panel-head"> | |
| 228 | - | <label for="refs-filter">ref></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))"> | |
| 230 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 255 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 257 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 258 | + | <kbd>esc</kbd> | |
| 259 | + | </button> | |
| 231 | 260 | </div> | |
| 232 | 261 | <div class="panel-cols" data-picklist> | |
| 233 | 262 | <section aria-label="Branches"> | |
| 234 | - | <h2>branches <span class="mute">2</span> | |
| 263 | + | <h2>Branches <span class="count">2</span> | |
| 235 | 264 | </h2> | |
| 236 | 265 | <ul> | |
| 237 | 266 | <li> | |
| @@ -245,7 +274,7 @@ | |||
| 245 | 274 | </ul> | |
| 246 | 275 | </section> | |
| 247 | 276 | <section aria-label="Tags"> | |
| 248 | - | <h2>tags <span class="mute">1</span> | |
| 277 | + | <h2>Tags <span class="count">1</span> | |
| 249 | 278 | </h2> | |
| 250 | 279 | <ul> | |
| 251 | 280 | <li> | |
| @@ -256,106 +285,124 @@ | |||
| 256 | 285 | </section> | |
| 257 | 286 | </div> | |
| 258 | 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 | 289 | <div class="panel-head"> | |
| 261 | - | <label for="cmd-q" class="hi">:</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(); | |
| 290 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 292 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 264 | 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 | 294 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 266 | 295 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 267 | 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 | 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 | 301 | </div> | |
| 271 | - | <ul id="cmd-list" class="cmd-list"> | |
| 302 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 272 | 303 | <li data-k="go home" data-cursor> | |
| 273 | 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 | 308 | </li> | |
| 276 | 309 | <li data-k="section summary"> | |
| 277 | 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 | 314 | </li> | |
| 280 | - | <li data-k="section tree"> | |
| 315 | + | <li data-k="section files"> | |
| 281 | 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 | 320 | </li> | |
| 284 | 321 | <li data-k="section commits"> | |
| 285 | 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 | 326 | </li> | |
| 288 | - | <li data-k="section refs"> | |
| 327 | + | <li data-k="section branches & tags"> | |
| 289 | 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 | 332 | </li> | |
| 292 | 333 | <li data-k="section merge requests"> | |
| 293 | 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 | 338 | </li> | |
| 296 | 339 | <li data-k="branch feature/x"> | |
| 297 | 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 | 344 | </li> | |
| 300 | 345 | <li data-k="branch main"> | |
| 301 | 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 | 350 | </li> | |
| 304 | 351 | <li data-k="tag v1.0"> | |
| 305 | 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 | 356 | </li> | |
| 308 | 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 | 367 | </dialog> | |
| 310 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 368 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 326 | 369 | if (q('dialog[open]').count) return; | |
| 327 | 370 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 328 | 371 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 329 | 372 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 330 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 331 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 373 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 377 | })(1); | |
| 333 | - | k-k from:body -> ((d) => { | |
| 378 | + | k-k from:body -> (async (d) => { | |
| 334 | 379 | if (q('dialog[open]').count) return; | |
| 335 | 380 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 336 | 381 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 337 | 382 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 338 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 339 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 383 | + | 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' }); | |
| 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 | 387 | })(-1); | |
| 341 | 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 | 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 | 390 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 344 | 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(); | |
| 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()); | |
| 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(); | |
| 392 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 395 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 349 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 350 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 351 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 352 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 353 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 354 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 355 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 356 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 357 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 358 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 396 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 397 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 398 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 399 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 400 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 401 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 402 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 403 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 404 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 405 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 359 | 406 | k-next from:body -> { | |
| 360 | 407 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 361 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>notes.txt · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,36 +42,55 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <h1 class="file-h"> | |
| 66 | 89 | <span>notes.txt</span> | |
| 67 | 90 | <span class="acts"> | |
| 68 | - | <a href="/files/raw/main/notes.txt">raw</a> | |
| 69 | - | <a href="/files/commits/main/notes.txt">history</a> | |
| 70 | - | <a href="/files/tree/main">dir</a> | |
| 91 | + | <a class="btn" href="/files/raw/main/notes.txt">Raw</a> | |
| 92 | + | <a class="btn" href="/files/commits/main/notes.txt">History</a> | |
| 93 | + | <a class="btn" href="/files/tree/main">Folder</a> | |
| 71 | 94 | </span> | |
| 72 | 95 | </h1> | |
| 73 | 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 | 109 | </pre> | |
| 87 | 110 | </main> | |
| 88 | 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 | 112 | <section aria-labelledby="r-file"> | |
| 92 | - | <h2 id="r-file">file</h2> | |
| 113 | + | <h2 id="r-file">File</h2> | |
| 93 | 114 | <dl class="kv"> | |
| 94 | 115 | <dt>size</dt> | |
| 95 | 116 | <dd>24 B</dd> | |
| @@ -100,7 +121,7 @@ | |||
| 100 | 121 | </dl> | |
| 101 | 122 | </section> | |
| 102 | 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 | 125 | </h2> | |
| 105 | 126 | <p> | |
| 106 | 127 | <a href="/files/commit/SHA40">SHA7</a> add notes.txt</p> | |
| @@ -108,77 +129,77 @@ | |||
| 108 | 129 | </p> | |
| 109 | 130 | </section> | |
| 110 | 131 | <section aria-labelledby="r-sel"> | |
| 111 | - | <h2 id="r-sel">selection</h2> | |
| 132 | + | <h2 id="r-sel">Selection</h2> | |
| 112 | 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 | 134 | <p class="dim">y copies a permalink pinned to the commit sha</p> | |
| 114 | 135 | </section> | |
| 115 | - | </details> | |
| 116 | 136 | </aside> | |
| 117 | 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 | 153 | <div class="panel-head"> | |
| 120 | - | <span>keys</span> | |
| 121 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 154 | + | <h2>Keyboard shortcuts</h2> | |
| 155 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 156 | + | <kbd>esc</kbd> close</button> | |
| 122 | 157 | </div> | |
| 123 | 158 | <dl class="keys"> | |
| 124 | 159 | <div> | |
| 125 | 160 | <dt> | |
| 126 | - | <kbd>j</kbd> | |
| 127 | - | <kbd>k</kbd> | |
| 161 | + | <kbd>ctrl k</kbd> | |
| 162 | + | <kbd>:</kbd> | |
| 128 | 163 | </dt> | |
| 129 | - | <dd>move cursor down / up</dd> | |
| 164 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 130 | 165 | </div> | |
| 131 | 166 | <div> | |
| 132 | 167 | <dt> | |
| 133 | - | <kbd>enter</kbd> | |
| 168 | + | <kbd>?</kbd> | |
| 134 | 169 | </dt> | |
| 135 | - | <dd>open row under cursor</dd> | |
| 170 | + | <dd>this list</dd> | |
| 136 | 171 | </div> | |
| 137 | 172 | <div> | |
| 138 | 173 | <dt> | |
| 139 | - | <kbd>g</kbd> | |
| 140 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 174 | + | <kbd>j</kbd> | |
| 175 | + | <kbd>k</kbd> | |
| 141 | 176 | </dt> | |
| 142 | - | <dd>first / last row</dd> | |
| 177 | + | <dd>move the row cursor down / up</dd> | |
| 143 | 178 | </div> | |
| 144 | 179 | <div> | |
| 145 | 180 | <dt> | |
| 146 | - | <kbd>/</kbd> | |
| 181 | + | <kbd>enter</kbd> | |
| 147 | 182 | </dt> | |
| 148 | - | <dd>focus the filter</dd> | |
| 183 | + | <dd>open the row under the cursor</dd> | |
| 149 | 184 | </div> | |
| 150 | 185 | <div> | |
| 151 | 186 | <dt> | |
| 152 | - | <kbd>g h</kbd> | |
| 153 | - | <kbd>g t</kbd> | |
| 187 | + | <kbd>g g</kbd> | |
| 188 | + | <kbd>G</kbd> | |
| 154 | 189 | </dt> | |
| 155 | - | <dd>go home / tree</dd> | |
| 190 | + | <dd>first / last row</dd> | |
| 156 | 191 | </div> | |
| 157 | 192 | <div> | |
| 158 | 193 | <dt> | |
| 159 | - | <kbd>g c</kbd> | |
| 160 | - | <kbd>g r</kbd> | |
| 194 | + | <kbd>/</kbd> | |
| 161 | 195 | </dt> | |
| 162 | - | <dd>go commits / refs</dd> | |
| 196 | + | <dd>filter the list</dd> | |
| 163 | 197 | </div> | |
| 164 | 198 | <div> | |
| 165 | 199 | <dt> | |
| 166 | 200 | <kbd>b</kbd> | |
| 167 | 201 | </dt> | |
| 168 | - | <dd>switch ref (branches, tags)</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> | |
| 202 | + | <dd>switch branch or tag</dd> | |
| 182 | 203 | </div> | |
| 183 | 204 | <div> | |
| 184 | 205 | <dt> | |
| @@ -186,15 +207,21 @@ | |||
| 186 | 207 | <kbd>t</kbd> | |
| 187 | 208 | <kbd>c</kbd> | |
| 188 | 209 | <kbd>r</kbd> | |
| 210 | + | <kbd>m</kbd> | |
| 189 | 211 | </dt> | |
| 190 | - | <dd>repo sections (when shown)</dd> | |
| 212 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 191 | 213 | </div> | |
| 192 | 214 | <div> | |
| 193 | 215 | <dt> | |
| 194 | - | <kbd>m</kbd> | |
| 195 | - | <kbd>g m</kbd> | |
| 216 | + | <kbd>g h</kbd> | |
| 217 | + | </dt> | |
| 218 | + | <dd>home</dd> | |
| 219 | + | </div> | |
| 220 | + | <div> | |
| 221 | + | <dt> | |
| 222 | + | <kbd>y</kbd> | |
| 196 | 223 | </dt> | |
| 197 | - | <dd>merge requests (when shown)</dd> | |
| 224 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 198 | 225 | </div> | |
| 199 | 226 | <div> | |
| 200 | 227 | <dt> | |
| @@ -214,19 +241,21 @@ | |||
| 214 | 241 | <dt> | |
| 215 | 242 | <kbd>esc</kbd> | |
| 216 | 243 | </dt> | |
| 217 | - | <dd>close panel</dd> | |
| 244 | + | <dd>close a dialog, leave a field</dd> | |
| 218 | 245 | </div> | |
| 219 | 246 | </dl> | |
| 220 | 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 | 249 | <div class="panel-head"> | |
| 223 | - | <label for="refs-filter">ref></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))"> | |
| 225 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 250 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 252 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 253 | + | <kbd>esc</kbd> | |
| 254 | + | </button> | |
| 226 | 255 | </div> | |
| 227 | 256 | <div class="panel-cols" data-picklist> | |
| 228 | 257 | <section aria-label="Branches"> | |
| 229 | - | <h2>branches <span class="mute">2</span> | |
| 258 | + | <h2>Branches <span class="count">2</span> | |
| 230 | 259 | </h2> | |
| 231 | 260 | <ul> | |
| 232 | 261 | <li> | |
| @@ -240,7 +269,7 @@ | |||
| 240 | 269 | </ul> | |
| 241 | 270 | </section> | |
| 242 | 271 | <section aria-label="Tags"> | |
| 243 | - | <h2>tags <span class="mute">1</span> | |
| 272 | + | <h2>Tags <span class="count">1</span> | |
| 244 | 273 | </h2> | |
| 245 | 274 | <ul> | |
| 246 | 275 | <li> | |
| @@ -251,106 +280,124 @@ | |||
| 251 | 280 | </section> | |
| 252 | 281 | </div> | |
| 253 | 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 | 284 | <div class="panel-head"> | |
| 256 | - | <label for="cmd-q" class="hi">:</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(); | |
| 285 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 287 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 259 | 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 | 289 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 261 | 290 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 262 | 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 | 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 | 296 | </div> | |
| 266 | - | <ul id="cmd-list" class="cmd-list"> | |
| 297 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 267 | 298 | <li data-k="go home" data-cursor> | |
| 268 | 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 | 303 | </li> | |
| 271 | 304 | <li data-k="section summary"> | |
| 272 | 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 | 309 | </li> | |
| 275 | - | <li data-k="section tree"> | |
| 310 | + | <li data-k="section files"> | |
| 276 | 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 | 315 | </li> | |
| 279 | 316 | <li data-k="section commits"> | |
| 280 | 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 | 321 | </li> | |
| 283 | - | <li data-k="section refs"> | |
| 322 | + | <li data-k="section branches & tags"> | |
| 284 | 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 | 327 | </li> | |
| 287 | 328 | <li data-k="section merge requests"> | |
| 288 | 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 | 333 | </li> | |
| 291 | 334 | <li data-k="branch feature/x"> | |
| 292 | 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 | 339 | </li> | |
| 295 | 340 | <li data-k="branch main"> | |
| 296 | 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 | 345 | </li> | |
| 299 | 346 | <li data-k="tag v1.0"> | |
| 300 | 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 | 351 | </li> | |
| 303 | 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 | 362 | </dialog> | |
| 305 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 363 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 321 | 364 | if (q('dialog[open]').count) return; | |
| 322 | 365 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 323 | 366 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 324 | 367 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 325 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 326 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 368 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 372 | })(1); | |
| 328 | - | k-k from:body -> ((d) => { | |
| 373 | + | k-k from:body -> (async (d) => { | |
| 329 | 374 | if (q('dialog[open]').count) return; | |
| 330 | 375 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 331 | 376 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 332 | 377 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 333 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 334 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 378 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 382 | })(-1); | |
| 336 | 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 | 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 | 385 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 339 | 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(); | |
| 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()); | |
| 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(); | |
| 387 | + | 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].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].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 343 | 390 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 344 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 345 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 346 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 347 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 348 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 349 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 350 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 351 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 352 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 353 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 391 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 392 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 393 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 394 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 395 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 396 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 397 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 398 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 399 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 400 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 354 | 401 | k-next from:body -> { | |
| 355 | 402 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 356 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>big.txt · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,45 +42,62 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <h1 class="file-h"> | |
| 66 | 89 | <span>big.txt</span> | |
| 67 | 90 | <span class="acts"> | |
| 68 | - | <a href="/files/raw/main/big.txt">raw</a> | |
| 69 | - | <a href="/files/commits/main/big.txt">history</a> | |
| 70 | - | <a href="/files/tree/main">dir</a> | |
| 91 | + | <a class="btn" href="/files/raw/main/big.txt">Raw</a> | |
| 92 | + | <a class="btn" href="/files/commits/main/big.txt">History</a> | |
| 93 | + | <a class="btn" href="/files/tree/main">Folder</a> | |
| 71 | 94 | </span> | |
| 72 | 95 | </h1> | |
| 73 | 96 | <p class="notice" role="note">file too large to show (1.1 MiB, the limit is 1.0 MiB). use raw.</p> | |
| 74 | 97 | </main> | |
| 75 | 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 | 99 | <section aria-labelledby="r-file"> | |
| 79 | - | <h2 id="r-file">file</h2> | |
| 100 | + | <h2 id="r-file">File</h2> | |
| 80 | 101 | <dl class="kv"> | |
| 81 | 102 | <dt>size</dt> | |
| 82 | 103 | <dd>1.1 MiB</dd> | |
| @@ -85,80 +106,80 @@ | |||
| 85 | 106 | </dl> | |
| 86 | 107 | </section> | |
| 87 | 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 | 110 | </h2> | |
| 90 | 111 | <p> | |
| 91 | 112 | <a href="/files/commit/SHA40">SHA7</a> add big.txt</p> | |
| 92 | 113 | <p class="dim">T · <time datetime="STAMP" title="STAMP">AGO</time> | |
| 93 | 114 | </p> | |
| 94 | 115 | </section> | |
| 95 | - | </details> | |
| 96 | 116 | </aside> | |
| 97 | 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 | 133 | <div class="panel-head"> | |
| 100 | - | <span>keys</span> | |
| 101 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 134 | + | <h2>Keyboard shortcuts</h2> | |
| 135 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 136 | + | <kbd>esc</kbd> close</button> | |
| 102 | 137 | </div> | |
| 103 | 138 | <dl class="keys"> | |
| 104 | 139 | <div> | |
| 105 | 140 | <dt> | |
| 106 | - | <kbd>j</kbd> | |
| 107 | - | <kbd>k</kbd> | |
| 141 | + | <kbd>ctrl k</kbd> | |
| 142 | + | <kbd>:</kbd> | |
| 108 | 143 | </dt> | |
| 109 | - | <dd>move cursor down / up</dd> | |
| 144 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 110 | 145 | </div> | |
| 111 | 146 | <div> | |
| 112 | 147 | <dt> | |
| 113 | - | <kbd>enter</kbd> | |
| 148 | + | <kbd>?</kbd> | |
| 114 | 149 | </dt> | |
| 115 | - | <dd>open row under cursor</dd> | |
| 150 | + | <dd>this list</dd> | |
| 116 | 151 | </div> | |
| 117 | 152 | <div> | |
| 118 | 153 | <dt> | |
| 119 | - | <kbd>g</kbd> | |
| 120 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 154 | + | <kbd>j</kbd> | |
| 155 | + | <kbd>k</kbd> | |
| 121 | 156 | </dt> | |
| 122 | - | <dd>first / last row</dd> | |
| 157 | + | <dd>move the row cursor down / up</dd> | |
| 123 | 158 | </div> | |
| 124 | 159 | <div> | |
| 125 | 160 | <dt> | |
| 126 | - | <kbd>/</kbd> | |
| 161 | + | <kbd>enter</kbd> | |
| 127 | 162 | </dt> | |
| 128 | - | <dd>focus the filter</dd> | |
| 163 | + | <dd>open the row under the cursor</dd> | |
| 129 | 164 | </div> | |
| 130 | 165 | <div> | |
| 131 | 166 | <dt> | |
| 132 | - | <kbd>g h</kbd> | |
| 133 | - | <kbd>g t</kbd> | |
| 167 | + | <kbd>g g</kbd> | |
| 168 | + | <kbd>G</kbd> | |
| 134 | 169 | </dt> | |
| 135 | - | <dd>go home / tree</dd> | |
| 170 | + | <dd>first / last row</dd> | |
| 136 | 171 | </div> | |
| 137 | 172 | <div> | |
| 138 | 173 | <dt> | |
| 139 | - | <kbd>g c</kbd> | |
| 140 | - | <kbd>g r</kbd> | |
| 174 | + | <kbd>/</kbd> | |
| 141 | 175 | </dt> | |
| 142 | - | <dd>go commits / refs</dd> | |
| 176 | + | <dd>filter the list</dd> | |
| 143 | 177 | </div> | |
| 144 | 178 | <div> | |
| 145 | 179 | <dt> | |
| 146 | 180 | <kbd>b</kbd> | |
| 147 | 181 | </dt> | |
| 148 | - | <dd>switch ref (branches, tags)</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> | |
| 182 | + | <dd>switch branch or tag</dd> | |
| 162 | 183 | </div> | |
| 163 | 184 | <div> | |
| 164 | 185 | <dt> | |
| @@ -166,15 +187,21 @@ | |||
| 166 | 187 | <kbd>t</kbd> | |
| 167 | 188 | <kbd>c</kbd> | |
| 168 | 189 | <kbd>r</kbd> | |
| 190 | + | <kbd>m</kbd> | |
| 169 | 191 | </dt> | |
| 170 | - | <dd>repo sections (when shown)</dd> | |
| 192 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 171 | 193 | </div> | |
| 172 | 194 | <div> | |
| 173 | 195 | <dt> | |
| 174 | - | <kbd>m</kbd> | |
| 175 | - | <kbd>g m</kbd> | |
| 196 | + | <kbd>g h</kbd> | |
| 197 | + | </dt> | |
| 198 | + | <dd>home</dd> | |
| 199 | + | </div> | |
| 200 | + | <div> | |
| 201 | + | <dt> | |
| 202 | + | <kbd>y</kbd> | |
| 176 | 203 | </dt> | |
| 177 | - | <dd>merge requests (when shown)</dd> | |
| 204 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 178 | 205 | </div> | |
| 179 | 206 | <div> | |
| 180 | 207 | <dt> | |
| @@ -194,19 +221,21 @@ | |||
| 194 | 221 | <dt> | |
| 195 | 222 | <kbd>esc</kbd> | |
| 196 | 223 | </dt> | |
| 197 | - | <dd>close panel</dd> | |
| 224 | + | <dd>close a dialog, leave a field</dd> | |
| 198 | 225 | </div> | |
| 199 | 226 | </dl> | |
| 200 | 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 | 229 | <div class="panel-head"> | |
| 203 | - | <label for="refs-filter">ref></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))"> | |
| 205 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 230 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 232 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 233 | + | <kbd>esc</kbd> | |
| 234 | + | </button> | |
| 206 | 235 | </div> | |
| 207 | 236 | <div class="panel-cols" data-picklist> | |
| 208 | 237 | <section aria-label="Branches"> | |
| 209 | - | <h2>branches <span class="mute">2</span> | |
| 238 | + | <h2>Branches <span class="count">2</span> | |
| 210 | 239 | </h2> | |
| 211 | 240 | <ul> | |
| 212 | 241 | <li> | |
| @@ -220,7 +249,7 @@ | |||
| 220 | 249 | </ul> | |
| 221 | 250 | </section> | |
| 222 | 251 | <section aria-label="Tags"> | |
| 223 | - | <h2>tags <span class="mute">1</span> | |
| 252 | + | <h2>Tags <span class="count">1</span> | |
| 224 | 253 | </h2> | |
| 225 | 254 | <ul> | |
| 226 | 255 | <li> | |
| @@ -231,105 +260,124 @@ | |||
| 231 | 260 | </section> | |
| 232 | 261 | </div> | |
| 233 | 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 | 264 | <div class="panel-head"> | |
| 236 | - | <label for="cmd-q" class="hi">:</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(); | |
| 265 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 267 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 239 | 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 | 269 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 241 | 270 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 242 | 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 | 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 | 276 | </div> | |
| 246 | - | <ul id="cmd-list" class="cmd-list"> | |
| 277 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 247 | 278 | <li data-k="go home" data-cursor> | |
| 248 | 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 | 283 | </li> | |
| 251 | 284 | <li data-k="section summary"> | |
| 252 | 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 | 289 | </li> | |
| 255 | - | <li data-k="section tree"> | |
| 290 | + | <li data-k="section files"> | |
| 256 | 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 | 295 | </li> | |
| 259 | 296 | <li data-k="section commits"> | |
| 260 | 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 | 301 | </li> | |
| 263 | - | <li data-k="section refs"> | |
| 302 | + | <li data-k="section branches & tags"> | |
| 264 | 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 | 307 | </li> | |
| 267 | 308 | <li data-k="section merge requests"> | |
| 268 | 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 | 313 | </li> | |
| 271 | 314 | <li data-k="branch feature/x"> | |
| 272 | 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 | 319 | </li> | |
| 275 | 320 | <li data-k="branch main"> | |
| 276 | 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 | 325 | </li> | |
| 279 | 326 | <li data-k="tag v1.0"> | |
| 280 | 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 | 331 | </li> | |
| 283 | 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 | 342 | </dialog> | |
| 285 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 343 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 300 | 344 | if (q('dialog[open]').count) return; | |
| 301 | 345 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 302 | 346 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 303 | 347 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 304 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 305 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 348 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 352 | })(1); | |
| 307 | - | k-k from:body -> ((d) => { | |
| 353 | + | k-k from:body -> (async (d) => { | |
| 308 | 354 | if (q('dialog[open]').count) return; | |
| 309 | 355 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 310 | 356 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 311 | 357 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 312 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 313 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 358 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 362 | })(-1); | |
| 315 | 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 | 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 | 365 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 318 | 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(); | |
| 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()); | |
| 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(); | |
| 367 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 370 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 323 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 324 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 325 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 326 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 327 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 328 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 329 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 330 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 331 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 332 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 371 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 372 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 373 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 374 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 375 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 376 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 377 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 378 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 379 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 380 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 333 | 381 | k-next from:body -> { | |
| 334 | 382 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 335 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>evil.go · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,36 +42,55 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <h1 class="file-h"> | |
| 66 | 89 | <span>evil.go</span> | |
| 67 | 90 | <span class="acts"> | |
| 68 | - | <a href="/files/raw/main/evil.go">raw</a> | |
| 69 | - | <a href="/files/commits/main/evil.go">history</a> | |
| 70 | - | <a href="/files/tree/main">dir</a> | |
| 91 | + | <a class="btn" href="/files/raw/main/evil.go">Raw</a> | |
| 92 | + | <a class="btn" href="/files/commits/main/evil.go">History</a> | |
| 93 | + | <a class="btn" href="/files/tree/main">Folder</a> | |
| 71 | 94 | </span> | |
| 72 | 95 | </h1> | |
| 73 | 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 | 156 | </pre> | |
| 134 | 157 | </main> | |
| 135 | 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 | 159 | <section aria-labelledby="r-file"> | |
| 139 | - | <h2 id="r-file">file</h2> | |
| 160 | + | <h2 id="r-file">File</h2> | |
| 140 | 161 | <dl class="kv"> | |
| 141 | 162 | <dt>size</dt> | |
| 142 | 163 | <dd>87 B</dd> | |
| @@ -147,7 +168,7 @@ | |||
| 147 | 168 | </dl> | |
| 148 | 169 | </section> | |
| 149 | 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 | 172 | </h2> | |
| 152 | 173 | <p> | |
| 153 | 174 | <a href="/files/commit/SHA40">SHA7</a> add evil.go</p> | |
| @@ -155,77 +176,77 @@ | |||
| 155 | 176 | </p> | |
| 156 | 177 | </section> | |
| 157 | 178 | <section aria-labelledby="r-sel"> | |
| 158 | - | <h2 id="r-sel">selection</h2> | |
| 179 | + | <h2 id="r-sel">Selection</h2> | |
| 159 | 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 | 181 | <p class="dim">y copies a permalink pinned to the commit sha</p> | |
| 161 | 182 | </section> | |
| 162 | - | </details> | |
| 163 | 183 | </aside> | |
| 164 | 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 | 200 | <div class="panel-head"> | |
| 167 | - | <span>keys</span> | |
| 168 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 201 | + | <h2>Keyboard shortcuts</h2> | |
| 202 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 203 | + | <kbd>esc</kbd> close</button> | |
| 169 | 204 | </div> | |
| 170 | 205 | <dl class="keys"> | |
| 171 | 206 | <div> | |
| 172 | 207 | <dt> | |
| 173 | - | <kbd>j</kbd> | |
| 174 | - | <kbd>k</kbd> | |
| 208 | + | <kbd>ctrl k</kbd> | |
| 209 | + | <kbd>:</kbd> | |
| 175 | 210 | </dt> | |
| 176 | - | <dd>move cursor down / up</dd> | |
| 211 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 177 | 212 | </div> | |
| 178 | 213 | <div> | |
| 179 | 214 | <dt> | |
| 180 | - | <kbd>enter</kbd> | |
| 215 | + | <kbd>?</kbd> | |
| 181 | 216 | </dt> | |
| 182 | - | <dd>open row under cursor</dd> | |
| 217 | + | <dd>this list</dd> | |
| 183 | 218 | </div> | |
| 184 | 219 | <div> | |
| 185 | 220 | <dt> | |
| 186 | - | <kbd>g</kbd> | |
| 187 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 221 | + | <kbd>j</kbd> | |
| 222 | + | <kbd>k</kbd> | |
| 188 | 223 | </dt> | |
| 189 | - | <dd>first / last row</dd> | |
| 224 | + | <dd>move the row cursor down / up</dd> | |
| 190 | 225 | </div> | |
| 191 | 226 | <div> | |
| 192 | 227 | <dt> | |
| 193 | - | <kbd>/</kbd> | |
| 228 | + | <kbd>enter</kbd> | |
| 194 | 229 | </dt> | |
| 195 | - | <dd>focus the filter</dd> | |
| 230 | + | <dd>open the row under the cursor</dd> | |
| 196 | 231 | </div> | |
| 197 | 232 | <div> | |
| 198 | 233 | <dt> | |
| 199 | - | <kbd>g h</kbd> | |
| 200 | - | <kbd>g t</kbd> | |
| 234 | + | <kbd>g g</kbd> | |
| 235 | + | <kbd>G</kbd> | |
| 201 | 236 | </dt> | |
| 202 | - | <dd>go home / tree</dd> | |
| 237 | + | <dd>first / last row</dd> | |
| 203 | 238 | </div> | |
| 204 | 239 | <div> | |
| 205 | 240 | <dt> | |
| 206 | - | <kbd>g c</kbd> | |
| 207 | - | <kbd>g r</kbd> | |
| 241 | + | <kbd>/</kbd> | |
| 208 | 242 | </dt> | |
| 209 | - | <dd>go commits / refs</dd> | |
| 243 | + | <dd>filter the list</dd> | |
| 210 | 244 | </div> | |
| 211 | 245 | <div> | |
| 212 | 246 | <dt> | |
| 213 | 247 | <kbd>b</kbd> | |
| 214 | 248 | </dt> | |
| 215 | - | <dd>switch ref (branches, tags)</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> | |
| 249 | + | <dd>switch branch or tag</dd> | |
| 229 | 250 | </div> | |
| 230 | 251 | <div> | |
| 231 | 252 | <dt> | |
| @@ -233,15 +254,21 @@ | |||
| 233 | 254 | <kbd>t</kbd> | |
| 234 | 255 | <kbd>c</kbd> | |
| 235 | 256 | <kbd>r</kbd> | |
| 257 | + | <kbd>m</kbd> | |
| 236 | 258 | </dt> | |
| 237 | - | <dd>repo sections (when shown)</dd> | |
| 259 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 238 | 260 | </div> | |
| 239 | 261 | <div> | |
| 240 | 262 | <dt> | |
| 241 | - | <kbd>m</kbd> | |
| 242 | - | <kbd>g m</kbd> | |
| 263 | + | <kbd>g h</kbd> | |
| 264 | + | </dt> | |
| 265 | + | <dd>home</dd> | |
| 266 | + | </div> | |
| 267 | + | <div> | |
| 268 | + | <dt> | |
| 269 | + | <kbd>y</kbd> | |
| 243 | 270 | </dt> | |
| 244 | - | <dd>merge requests (when shown)</dd> | |
| 271 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 245 | 272 | </div> | |
| 246 | 273 | <div> | |
| 247 | 274 | <dt> | |
| @@ -261,19 +288,21 @@ | |||
| 261 | 288 | <dt> | |
| 262 | 289 | <kbd>esc</kbd> | |
| 263 | 290 | </dt> | |
| 264 | - | <dd>close panel</dd> | |
| 291 | + | <dd>close a dialog, leave a field</dd> | |
| 265 | 292 | </div> | |
| 266 | 293 | </dl> | |
| 267 | 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 | 296 | <div class="panel-head"> | |
| 270 | - | <label for="refs-filter">ref></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))"> | |
| 272 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 297 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 299 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 300 | + | <kbd>esc</kbd> | |
| 301 | + | </button> | |
| 273 | 302 | </div> | |
| 274 | 303 | <div class="panel-cols" data-picklist> | |
| 275 | 304 | <section aria-label="Branches"> | |
| 276 | - | <h2>branches <span class="mute">2</span> | |
| 305 | + | <h2>Branches <span class="count">2</span> | |
| 277 | 306 | </h2> | |
| 278 | 307 | <ul> | |
| 279 | 308 | <li> | |
| @@ -287,7 +316,7 @@ | |||
| 287 | 316 | </ul> | |
| 288 | 317 | </section> | |
| 289 | 318 | <section aria-label="Tags"> | |
| 290 | - | <h2>tags <span class="mute">1</span> | |
| 319 | + | <h2>Tags <span class="count">1</span> | |
| 291 | 320 | </h2> | |
| 292 | 321 | <ul> | |
| 293 | 322 | <li> | |
| @@ -298,106 +327,124 @@ | |||
| 298 | 327 | </section> | |
| 299 | 328 | </div> | |
| 300 | 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 | 331 | <div class="panel-head"> | |
| 303 | - | <label for="cmd-q" class="hi">:</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(); | |
| 332 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 334 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 306 | 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 | 336 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 308 | 337 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 309 | 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 | 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 | 343 | </div> | |
| 313 | - | <ul id="cmd-list" class="cmd-list"> | |
| 344 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 314 | 345 | <li data-k="go home" data-cursor> | |
| 315 | 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 | 350 | </li> | |
| 318 | 351 | <li data-k="section summary"> | |
| 319 | 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 | 356 | </li> | |
| 322 | - | <li data-k="section tree"> | |
| 357 | + | <li data-k="section files"> | |
| 323 | 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 | 362 | </li> | |
| 326 | 363 | <li data-k="section commits"> | |
| 327 | 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 | 368 | </li> | |
| 330 | - | <li data-k="section refs"> | |
| 369 | + | <li data-k="section branches & tags"> | |
| 331 | 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 | 374 | </li> | |
| 334 | 375 | <li data-k="section merge requests"> | |
| 335 | 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 | 380 | </li> | |
| 338 | 381 | <li data-k="branch feature/x"> | |
| 339 | 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 | 386 | </li> | |
| 342 | 387 | <li data-k="branch main"> | |
| 343 | 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 | 392 | </li> | |
| 346 | 393 | <li data-k="tag v1.0"> | |
| 347 | 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 | 398 | </li> | |
| 350 | 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 | 409 | </dialog> | |
| 352 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 410 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 368 | 411 | if (q('dialog[open]').count) return; | |
| 369 | 412 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 370 | 413 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 371 | 414 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 372 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 373 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 415 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 419 | })(1); | |
| 375 | - | k-k from:body -> ((d) => { | |
| 420 | + | k-k from:body -> (async (d) => { | |
| 376 | 421 | if (q('dialog[open]').count) return; | |
| 377 | 422 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 378 | 423 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 379 | 424 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 380 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 381 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 425 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 429 | })(-1); | |
| 383 | 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 | 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 | 432 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 386 | 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(); | |
| 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()); | |
| 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(); | |
| 434 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 437 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 391 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 392 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 393 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 394 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 395 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 396 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 397 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 398 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 399 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 400 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 438 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 439 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 440 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 441 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 442 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 443 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 444 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 445 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 446 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 447 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 401 | 448 | k-next from:body -> { | |
| 402 | 449 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 403 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>SHA7 add a very large file · diffs · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,30 +39,49 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <header class="show"> | |
| 63 | 86 | <h1>add a very large file</h1> | |
| 64 | 87 | <dl class="kv show-kv"> | |
| @@ -71,7 +94,9 @@ | |||
| 71 | 94 | <a href="/diffs/commit/SHA40">SHA7</a> | |
| 72 | 95 | </dd> | |
| 73 | 96 | <dt>refs</dt> | |
| 74 | - | <dd>[main] </dd> | |
| 97 | + | <dd> | |
| 98 | + | <span class="pill">main</span> | |
| 99 | + | </dd> | |
| 75 | 100 | </dl> | |
| 76 | 101 | </header> | |
| 77 | 102 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | |
| @@ -80,9 +105,12 @@ | |||
| 80 | 105 | </span> | |
| 81 | 106 | <span class="grow"> | |
| 82 | 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> | |
| 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> | |
| 85 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | |
| 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> | |
| 109 | + | </a> | |
| 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 | 114 | </div> | |
| 87 | 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 | 116 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| @@ -99,10 +127,8 @@ | |||
| 99 | 127 | </div> | |
| 100 | 128 | </main> | |
| 101 | 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 | 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 | 132 | </h2> | |
| 107 | 133 | <ol class="files" aria-label="Changed files"> | |
| 108 | 134 | <li> | |
| @@ -122,73 +148,73 @@ | |||
| 122 | 148 | </li> | |
| 123 | 149 | </ol> | |
| 124 | 150 | </section> | |
| 125 | - | </details> | |
| 126 | 151 | </aside> | |
| 127 | 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 | 168 | <div class="panel-head"> | |
| 130 | - | <span>keys</span> | |
| 131 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 169 | + | <h2>Keyboard shortcuts</h2> | |
| 170 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 171 | + | <kbd>esc</kbd> close</button> | |
| 132 | 172 | </div> | |
| 133 | 173 | <dl class="keys"> | |
| 134 | 174 | <div> | |
| 135 | 175 | <dt> | |
| 136 | - | <kbd>j</kbd> | |
| 137 | - | <kbd>k</kbd> | |
| 176 | + | <kbd>ctrl k</kbd> | |
| 177 | + | <kbd>:</kbd> | |
| 138 | 178 | </dt> | |
| 139 | - | <dd>move cursor down / up</dd> | |
| 179 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 140 | 180 | </div> | |
| 141 | 181 | <div> | |
| 142 | 182 | <dt> | |
| 143 | - | <kbd>enter</kbd> | |
| 183 | + | <kbd>?</kbd> | |
| 144 | 184 | </dt> | |
| 145 | - | <dd>open row under cursor</dd> | |
| 185 | + | <dd>this list</dd> | |
| 146 | 186 | </div> | |
| 147 | 187 | <div> | |
| 148 | 188 | <dt> | |
| 149 | - | <kbd>g</kbd> | |
| 150 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 189 | + | <kbd>j</kbd> | |
| 190 | + | <kbd>k</kbd> | |
| 151 | 191 | </dt> | |
| 152 | - | <dd>first / last row</dd> | |
| 192 | + | <dd>move the row cursor down / up</dd> | |
| 153 | 193 | </div> | |
| 154 | 194 | <div> | |
| 155 | 195 | <dt> | |
| 156 | - | <kbd>/</kbd> | |
| 196 | + | <kbd>enter</kbd> | |
| 157 | 197 | </dt> | |
| 158 | - | <dd>focus the filter</dd> | |
| 198 | + | <dd>open the row under the cursor</dd> | |
| 159 | 199 | </div> | |
| 160 | 200 | <div> | |
| 161 | 201 | <dt> | |
| 162 | - | <kbd>g h</kbd> | |
| 163 | - | <kbd>g t</kbd> | |
| 202 | + | <kbd>g g</kbd> | |
| 203 | + | <kbd>G</kbd> | |
| 164 | 204 | </dt> | |
| 165 | - | <dd>go home / tree</dd> | |
| 205 | + | <dd>first / last row</dd> | |
| 166 | 206 | </div> | |
| 167 | 207 | <div> | |
| 168 | 208 | <dt> | |
| 169 | - | <kbd>g c</kbd> | |
| 170 | - | <kbd>g r</kbd> | |
| 209 | + | <kbd>/</kbd> | |
| 171 | 210 | </dt> | |
| 172 | - | <dd>go commits / refs</dd> | |
| 211 | + | <dd>filter the list</dd> | |
| 173 | 212 | </div> | |
| 174 | 213 | <div> | |
| 175 | 214 | <dt> | |
| 176 | 215 | <kbd>b</kbd> | |
| 177 | 216 | </dt> | |
| 178 | - | <dd>switch ref (branches, tags)</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> | |
| 217 | + | <dd>switch branch or tag</dd> | |
| 192 | 218 | </div> | |
| 193 | 219 | <div> | |
| 194 | 220 | <dt> | |
| @@ -196,15 +222,21 @@ | |||
| 196 | 222 | <kbd>t</kbd> | |
| 197 | 223 | <kbd>c</kbd> | |
| 198 | 224 | <kbd>r</kbd> | |
| 225 | + | <kbd>m</kbd> | |
| 199 | 226 | </dt> | |
| 200 | - | <dd>repo sections (when shown)</dd> | |
| 227 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 201 | 228 | </div> | |
| 202 | 229 | <div> | |
| 203 | 230 | <dt> | |
| 204 | - | <kbd>m</kbd> | |
| 205 | - | <kbd>g m</kbd> | |
| 231 | + | <kbd>g h</kbd> | |
| 232 | + | </dt> | |
| 233 | + | <dd>home</dd> | |
| 234 | + | </div> | |
| 235 | + | <div> | |
| 236 | + | <dt> | |
| 237 | + | <kbd>y</kbd> | |
| 206 | 238 | </dt> | |
| 207 | - | <dd>merge requests (when shown)</dd> | |
| 239 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 208 | 240 | </div> | |
| 209 | 241 | <div> | |
| 210 | 242 | <dt> | |
| @@ -224,19 +256,21 @@ | |||
| 224 | 256 | <dt> | |
| 225 | 257 | <kbd>esc</kbd> | |
| 226 | 258 | </dt> | |
| 227 | - | <dd>close panel</dd> | |
| 259 | + | <dd>close a dialog, leave a field</dd> | |
| 228 | 260 | </div> | |
| 229 | 261 | </dl> | |
| 230 | 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 | 264 | <div class="panel-head"> | |
| 233 | - | <label for="refs-filter">ref></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))"> | |
| 235 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 265 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 267 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 268 | + | <kbd>esc</kbd> | |
| 269 | + | </button> | |
| 236 | 270 | </div> | |
| 237 | 271 | <div class="panel-cols" data-picklist> | |
| 238 | 272 | <section aria-label="Branches"> | |
| 239 | - | <h2>branches <span class="mute">2</span> | |
| 273 | + | <h2>Branches <span class="count">2</span> | |
| 240 | 274 | </h2> | |
| 241 | 275 | <ul> | |
| 242 | 276 | <li> | |
| @@ -250,7 +284,7 @@ | |||
| 250 | 284 | </ul> | |
| 251 | 285 | </section> | |
| 252 | 286 | <section aria-label="Tags"> | |
| 253 | - | <h2>tags <span class="mute">1</span> | |
| 287 | + | <h2>Tags <span class="count">1</span> | |
| 254 | 288 | </h2> | |
| 255 | 289 | <ul> | |
| 256 | 290 | <li> | |
| @@ -261,105 +295,124 @@ | |||
| 261 | 295 | </section> | |
| 262 | 296 | </div> | |
| 263 | 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 | 299 | <div class="panel-head"> | |
| 266 | - | <label for="cmd-q" class="hi">:</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(); | |
| 300 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 302 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 269 | 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 | 304 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 271 | 305 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 272 | 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 | 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 | 311 | </div> | |
| 276 | - | <ul id="cmd-list" class="cmd-list"> | |
| 312 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 277 | 313 | <li data-k="go home" data-cursor> | |
| 278 | 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 | 318 | </li> | |
| 281 | 319 | <li data-k="section summary"> | |
| 282 | 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 | 324 | </li> | |
| 285 | - | <li data-k="section tree"> | |
| 325 | + | <li data-k="section files"> | |
| 286 | 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 | 330 | </li> | |
| 289 | 331 | <li data-k="section commits"> | |
| 290 | 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 | 336 | </li> | |
| 293 | - | <li data-k="section refs"> | |
| 337 | + | <li data-k="section branches & tags"> | |
| 294 | 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 | 342 | </li> | |
| 297 | 343 | <li data-k="section merge requests"> | |
| 298 | 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 | 348 | </li> | |
| 301 | 349 | <li data-k="branch main"> | |
| 302 | 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 | 354 | </li> | |
| 305 | 355 | <li data-k="branch side"> | |
| 306 | 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 | 360 | </li> | |
| 309 | 361 | <li data-k="tag v1.0.0"> | |
| 310 | 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 | 366 | </li> | |
| 313 | 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 | 377 | </dialog> | |
| 315 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 378 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 330 | 379 | if (q('dialog[open]').count) return; | |
| 331 | 380 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 332 | 381 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 333 | 382 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 334 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 335 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 383 | + | 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' }); | |
| 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 | 387 | })(1); | |
| 337 | - | k-k from:body -> ((d) => { | |
| 388 | + | k-k from:body -> (async (d) => { | |
| 338 | 389 | if (q('dialog[open]').count) return; | |
| 339 | 390 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 340 | 391 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 341 | 392 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 342 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 343 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 393 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 397 | })(-1); | |
| 345 | 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 | 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 | 400 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 348 | 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(); | |
| 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()); | |
| 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(); | |
| 402 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 405 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 353 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 354 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 355 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 356 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 357 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 358 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 359 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 360 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 361 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 362 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 406 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 407 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 408 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 409 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 410 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 411 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 412 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 413 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 414 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 415 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 363 | 416 | k-next from:body -> { | |
| 364 | 417 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 365 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>SHA7 change a binary file · diffs · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,30 +39,49 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <header class="show"> | |
| 63 | 86 | <h1>change a binary file</h1> | |
| 64 | 87 | <dl class="kv show-kv"> | |
| @@ -78,9 +101,12 @@ | |||
| 78 | 101 | </span> | |
| 79 | 102 | <span class="grow"> | |
| 80 | 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> | |
| 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> | |
| 83 | - | <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=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> | |
| 105 | + | </a> | |
| 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 | 110 | </div> | |
| 85 | 111 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 86 | 112 | <details class="dfile" id="f1" data-file> | |
| @@ -96,10 +122,8 @@ | |||
| 96 | 122 | </div> | |
| 97 | 123 | </main> | |
| 98 | 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 | 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 | 127 | </h2> | |
| 104 | 128 | <ol class="files" aria-label="Changed files"> | |
| 105 | 129 | <li> | |
| @@ -119,73 +143,73 @@ | |||
| 119 | 143 | </li> | |
| 120 | 144 | </ol> | |
| 121 | 145 | </section> | |
| 122 | - | </details> | |
| 123 | 146 | </aside> | |
| 124 | 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 | 163 | <div class="panel-head"> | |
| 127 | - | <span>keys</span> | |
| 128 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 164 | + | <h2>Keyboard shortcuts</h2> | |
| 165 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 166 | + | <kbd>esc</kbd> close</button> | |
| 129 | 167 | </div> | |
| 130 | 168 | <dl class="keys"> | |
| 131 | 169 | <div> | |
| 132 | 170 | <dt> | |
| 133 | - | <kbd>j</kbd> | |
| 134 | - | <kbd>k</kbd> | |
| 171 | + | <kbd>ctrl k</kbd> | |
| 172 | + | <kbd>:</kbd> | |
| 135 | 173 | </dt> | |
| 136 | - | <dd>move cursor down / up</dd> | |
| 174 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 137 | 175 | </div> | |
| 138 | 176 | <div> | |
| 139 | 177 | <dt> | |
| 140 | - | <kbd>enter</kbd> | |
| 178 | + | <kbd>?</kbd> | |
| 141 | 179 | </dt> | |
| 142 | - | <dd>open row under cursor</dd> | |
| 180 | + | <dd>this list</dd> | |
| 143 | 181 | </div> | |
| 144 | 182 | <div> | |
| 145 | 183 | <dt> | |
| 146 | - | <kbd>g</kbd> | |
| 147 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 184 | + | <kbd>j</kbd> | |
| 185 | + | <kbd>k</kbd> | |
| 148 | 186 | </dt> | |
| 149 | - | <dd>first / last row</dd> | |
| 187 | + | <dd>move the row cursor down / up</dd> | |
| 150 | 188 | </div> | |
| 151 | 189 | <div> | |
| 152 | 190 | <dt> | |
| 153 | - | <kbd>/</kbd> | |
| 191 | + | <kbd>enter</kbd> | |
| 154 | 192 | </dt> | |
| 155 | - | <dd>focus the filter</dd> | |
| 193 | + | <dd>open the row under the cursor</dd> | |
| 156 | 194 | </div> | |
| 157 | 195 | <div> | |
| 158 | 196 | <dt> | |
| 159 | - | <kbd>g h</kbd> | |
| 160 | - | <kbd>g t</kbd> | |
| 197 | + | <kbd>g g</kbd> | |
| 198 | + | <kbd>G</kbd> | |
| 161 | 199 | </dt> | |
| 162 | - | <dd>go home / tree</dd> | |
| 200 | + | <dd>first / last row</dd> | |
| 163 | 201 | </div> | |
| 164 | 202 | <div> | |
| 165 | 203 | <dt> | |
| 166 | - | <kbd>g c</kbd> | |
| 167 | - | <kbd>g r</kbd> | |
| 204 | + | <kbd>/</kbd> | |
| 168 | 205 | </dt> | |
| 169 | - | <dd>go commits / refs</dd> | |
| 206 | + | <dd>filter the list</dd> | |
| 170 | 207 | </div> | |
| 171 | 208 | <div> | |
| 172 | 209 | <dt> | |
| 173 | 210 | <kbd>b</kbd> | |
| 174 | 211 | </dt> | |
| 175 | - | <dd>switch ref (branches, tags)</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> | |
| 212 | + | <dd>switch branch or tag</dd> | |
| 189 | 213 | </div> | |
| 190 | 214 | <div> | |
| 191 | 215 | <dt> | |
| @@ -193,15 +217,21 @@ | |||
| 193 | 217 | <kbd>t</kbd> | |
| 194 | 218 | <kbd>c</kbd> | |
| 195 | 219 | <kbd>r</kbd> | |
| 220 | + | <kbd>m</kbd> | |
| 196 | 221 | </dt> | |
| 197 | - | <dd>repo sections (when shown)</dd> | |
| 222 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 198 | 223 | </div> | |
| 199 | 224 | <div> | |
| 200 | 225 | <dt> | |
| 201 | - | <kbd>m</kbd> | |
| 202 | - | <kbd>g m</kbd> | |
| 226 | + | <kbd>g h</kbd> | |
| 227 | + | </dt> | |
| 228 | + | <dd>home</dd> | |
| 229 | + | </div> | |
| 230 | + | <div> | |
| 231 | + | <dt> | |
| 232 | + | <kbd>y</kbd> | |
| 203 | 233 | </dt> | |
| 204 | - | <dd>merge requests (when shown)</dd> | |
| 234 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 205 | 235 | </div> | |
| 206 | 236 | <div> | |
| 207 | 237 | <dt> | |
| @@ -221,19 +251,21 @@ | |||
| 221 | 251 | <dt> | |
| 222 | 252 | <kbd>esc</kbd> | |
| 223 | 253 | </dt> | |
| 224 | - | <dd>close panel</dd> | |
| 254 | + | <dd>close a dialog, leave a field</dd> | |
| 225 | 255 | </div> | |
| 226 | 256 | </dl> | |
| 227 | 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 | 259 | <div class="panel-head"> | |
| 230 | - | <label for="refs-filter">ref></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))"> | |
| 232 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 260 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 262 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 263 | + | <kbd>esc</kbd> | |
| 264 | + | </button> | |
| 233 | 265 | </div> | |
| 234 | 266 | <div class="panel-cols" data-picklist> | |
| 235 | 267 | <section aria-label="Branches"> | |
| 236 | - | <h2>branches <span class="mute">2</span> | |
| 268 | + | <h2>Branches <span class="count">2</span> | |
| 237 | 269 | </h2> | |
| 238 | 270 | <ul> | |
| 239 | 271 | <li> | |
| @@ -247,7 +279,7 @@ | |||
| 247 | 279 | </ul> | |
| 248 | 280 | </section> | |
| 249 | 281 | <section aria-label="Tags"> | |
| 250 | - | <h2>tags <span class="mute">1</span> | |
| 282 | + | <h2>Tags <span class="count">1</span> | |
| 251 | 283 | </h2> | |
| 252 | 284 | <ul> | |
| 253 | 285 | <li> | |
| @@ -258,105 +290,124 @@ | |||
| 258 | 290 | </section> | |
| 259 | 291 | </div> | |
| 260 | 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 | 294 | <div class="panel-head"> | |
| 263 | - | <label for="cmd-q" class="hi">:</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(); | |
| 295 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 297 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 266 | 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 | 299 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 268 | 300 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 269 | 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 | 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 | 306 | </div> | |
| 273 | - | <ul id="cmd-list" class="cmd-list"> | |
| 307 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 274 | 308 | <li data-k="go home" data-cursor> | |
| 275 | 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 | 313 | </li> | |
| 278 | 314 | <li data-k="section summary"> | |
| 279 | 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 | 319 | </li> | |
| 282 | - | <li data-k="section tree"> | |
| 320 | + | <li data-k="section files"> | |
| 283 | 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 | 325 | </li> | |
| 286 | 326 | <li data-k="section commits"> | |
| 287 | 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 | 331 | </li> | |
| 290 | - | <li data-k="section refs"> | |
| 332 | + | <li data-k="section branches & tags"> | |
| 291 | 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 | 337 | </li> | |
| 294 | 338 | <li data-k="section merge requests"> | |
| 295 | 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 | 343 | </li> | |
| 298 | 344 | <li data-k="branch main"> | |
| 299 | 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 | 349 | </li> | |
| 302 | 350 | <li data-k="branch side"> | |
| 303 | 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 | 355 | </li> | |
| 306 | 356 | <li data-k="tag v1.0.0"> | |
| 307 | 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 | 361 | </li> | |
| 310 | 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 | 372 | </dialog> | |
| 312 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 373 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 327 | 374 | if (q('dialog[open]').count) return; | |
| 328 | 375 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 329 | 376 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 330 | 377 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 331 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 332 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 378 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 382 | })(1); | |
| 334 | - | k-k from:body -> ((d) => { | |
| 383 | + | k-k from:body -> (async (d) => { | |
| 335 | 384 | if (q('dialog[open]').count) return; | |
| 336 | 385 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 337 | 386 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 338 | 387 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 339 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 340 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 388 | + | 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' }); | |
| 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 | 392 | })(-1); | |
| 342 | 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 | 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 | 395 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 345 | 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(); | |
| 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()); | |
| 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(); | |
| 397 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 400 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 350 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 351 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 352 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 353 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 354 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 355 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 356 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 357 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 358 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 359 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 401 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 402 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 403 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 404 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 405 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 406 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 407 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 408 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 409 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 410 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 360 | 411 | k-next from:body -> { | |
| 361 | 412 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 362 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>SHA7 merge the side branch · diffs · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,30 +39,49 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <header class="show"> | |
| 63 | 86 | <h1>merge the side branch</h1> | |
| 64 | 87 | <dl class="kv show-kv"> | |
| @@ -77,9 +100,12 @@ | |||
| 77 | 100 | </span> | |
| 78 | 101 | <span class="grow"> | |
| 79 | 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> | |
| 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> | |
| 82 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | |
| 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> | |
| 104 | + | </a> | |
| 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 | 109 | </div> | |
| 84 | 110 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 85 | 111 | <details class="dfile" id="f1" data-file open> | |
| @@ -127,10 +153,8 @@ | |||
| 127 | 153 | </div> | |
| 128 | 154 | </main> | |
| 129 | 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 | 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 | 158 | </h2> | |
| 135 | 159 | <ol class="files" aria-label="Changed files"> | |
| 136 | 160 | <li> | |
| @@ -150,73 +174,73 @@ | |||
| 150 | 174 | </li> | |
| 151 | 175 | </ol> | |
| 152 | 176 | </section> | |
| 153 | - | </details> | |
| 154 | 177 | </aside> | |
| 155 | 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 | 194 | <div class="panel-head"> | |
| 158 | - | <span>keys</span> | |
| 159 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 195 | + | <h2>Keyboard shortcuts</h2> | |
| 196 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 197 | + | <kbd>esc</kbd> close</button> | |
| 160 | 198 | </div> | |
| 161 | 199 | <dl class="keys"> | |
| 162 | 200 | <div> | |
| 163 | 201 | <dt> | |
| 164 | - | <kbd>j</kbd> | |
| 165 | - | <kbd>k</kbd> | |
| 202 | + | <kbd>ctrl k</kbd> | |
| 203 | + | <kbd>:</kbd> | |
| 166 | 204 | </dt> | |
| 167 | - | <dd>move cursor down / up</dd> | |
| 205 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 168 | 206 | </div> | |
| 169 | 207 | <div> | |
| 170 | 208 | <dt> | |
| 171 | - | <kbd>enter</kbd> | |
| 209 | + | <kbd>?</kbd> | |
| 172 | 210 | </dt> | |
| 173 | - | <dd>open row under cursor</dd> | |
| 211 | + | <dd>this list</dd> | |
| 174 | 212 | </div> | |
| 175 | 213 | <div> | |
| 176 | 214 | <dt> | |
| 177 | - | <kbd>g</kbd> | |
| 178 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 215 | + | <kbd>j</kbd> | |
| 216 | + | <kbd>k</kbd> | |
| 179 | 217 | </dt> | |
| 180 | - | <dd>first / last row</dd> | |
| 218 | + | <dd>move the row cursor down / up</dd> | |
| 181 | 219 | </div> | |
| 182 | 220 | <div> | |
| 183 | 221 | <dt> | |
| 184 | - | <kbd>/</kbd> | |
| 222 | + | <kbd>enter</kbd> | |
| 185 | 223 | </dt> | |
| 186 | - | <dd>focus the filter</dd> | |
| 224 | + | <dd>open the row under the cursor</dd> | |
| 187 | 225 | </div> | |
| 188 | 226 | <div> | |
| 189 | 227 | <dt> | |
| 190 | - | <kbd>g h</kbd> | |
| 191 | - | <kbd>g t</kbd> | |
| 228 | + | <kbd>g g</kbd> | |
| 229 | + | <kbd>G</kbd> | |
| 192 | 230 | </dt> | |
| 193 | - | <dd>go home / tree</dd> | |
| 231 | + | <dd>first / last row</dd> | |
| 194 | 232 | </div> | |
| 195 | 233 | <div> | |
| 196 | 234 | <dt> | |
| 197 | - | <kbd>g c</kbd> | |
| 198 | - | <kbd>g r</kbd> | |
| 235 | + | <kbd>/</kbd> | |
| 199 | 236 | </dt> | |
| 200 | - | <dd>go commits / refs</dd> | |
| 237 | + | <dd>filter the list</dd> | |
| 201 | 238 | </div> | |
| 202 | 239 | <div> | |
| 203 | 240 | <dt> | |
| 204 | 241 | <kbd>b</kbd> | |
| 205 | 242 | </dt> | |
| 206 | - | <dd>switch ref (branches, tags)</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> | |
| 243 | + | <dd>switch branch or tag</dd> | |
| 220 | 244 | </div> | |
| 221 | 245 | <div> | |
| 222 | 246 | <dt> | |
| @@ -224,15 +248,21 @@ | |||
| 224 | 248 | <kbd>t</kbd> | |
| 225 | 249 | <kbd>c</kbd> | |
| 226 | 250 | <kbd>r</kbd> | |
| 251 | + | <kbd>m</kbd> | |
| 227 | 252 | </dt> | |
| 228 | - | <dd>repo sections (when shown)</dd> | |
| 253 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 229 | 254 | </div> | |
| 230 | 255 | <div> | |
| 231 | 256 | <dt> | |
| 232 | - | <kbd>m</kbd> | |
| 233 | - | <kbd>g m</kbd> | |
| 257 | + | <kbd>g h</kbd> | |
| 258 | + | </dt> | |
| 259 | + | <dd>home</dd> | |
| 260 | + | </div> | |
| 261 | + | <div> | |
| 262 | + | <dt> | |
| 263 | + | <kbd>y</kbd> | |
| 234 | 264 | </dt> | |
| 235 | - | <dd>merge requests (when shown)</dd> | |
| 265 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 236 | 266 | </div> | |
| 237 | 267 | <div> | |
| 238 | 268 | <dt> | |
| @@ -252,19 +282,21 @@ | |||
| 252 | 282 | <dt> | |
| 253 | 283 | <kbd>esc</kbd> | |
| 254 | 284 | </dt> | |
| 255 | - | <dd>close panel</dd> | |
| 285 | + | <dd>close a dialog, leave a field</dd> | |
| 256 | 286 | </div> | |
| 257 | 287 | </dl> | |
| 258 | 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 | 290 | <div class="panel-head"> | |
| 261 | - | <label for="refs-filter">ref></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))"> | |
| 263 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 291 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 293 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 294 | + | <kbd>esc</kbd> | |
| 295 | + | </button> | |
| 264 | 296 | </div> | |
| 265 | 297 | <div class="panel-cols" data-picklist> | |
| 266 | 298 | <section aria-label="Branches"> | |
| 267 | - | <h2>branches <span class="mute">2</span> | |
| 299 | + | <h2>Branches <span class="count">2</span> | |
| 268 | 300 | </h2> | |
| 269 | 301 | <ul> | |
| 270 | 302 | <li> | |
| @@ -278,7 +310,7 @@ | |||
| 278 | 310 | </ul> | |
| 279 | 311 | </section> | |
| 280 | 312 | <section aria-label="Tags"> | |
| 281 | - | <h2>tags <span class="mute">1</span> | |
| 313 | + | <h2>Tags <span class="count">1</span> | |
| 282 | 314 | </h2> | |
| 283 | 315 | <ul> | |
| 284 | 316 | <li> | |
| @@ -289,105 +321,124 @@ | |||
| 289 | 321 | </section> | |
| 290 | 322 | </div> | |
| 291 | 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 | 325 | <div class="panel-head"> | |
| 294 | - | <label for="cmd-q" class="hi">:</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(); | |
| 326 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 328 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 297 | 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 | 330 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 299 | 331 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 300 | 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 | 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 | 337 | </div> | |
| 304 | - | <ul id="cmd-list" class="cmd-list"> | |
| 338 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 305 | 339 | <li data-k="go home" data-cursor> | |
| 306 | 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 | 344 | </li> | |
| 309 | 345 | <li data-k="section summary"> | |
| 310 | 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 | 350 | </li> | |
| 313 | - | <li data-k="section tree"> | |
| 351 | + | <li data-k="section files"> | |
| 314 | 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 | 356 | </li> | |
| 317 | 357 | <li data-k="section commits"> | |
| 318 | 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 | 362 | </li> | |
| 321 | - | <li data-k="section refs"> | |
| 363 | + | <li data-k="section branches & tags"> | |
| 322 | 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 | 368 | </li> | |
| 325 | 369 | <li data-k="section merge requests"> | |
| 326 | 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 | 374 | </li> | |
| 329 | 375 | <li data-k="branch main"> | |
| 330 | 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 | 380 | </li> | |
| 333 | 381 | <li data-k="branch side"> | |
| 334 | 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 | 386 | </li> | |
| 337 | 387 | <li data-k="tag v1.0.0"> | |
| 338 | 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 | 392 | </li> | |
| 341 | 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 | 403 | </dialog> | |
| 343 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 404 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 358 | 405 | if (q('dialog[open]').count) return; | |
| 359 | 406 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 360 | 407 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 361 | 408 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 362 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 363 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 409 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 413 | })(1); | |
| 365 | - | k-k from:body -> ((d) => { | |
| 414 | + | k-k from:body -> (async (d) => { | |
| 366 | 415 | if (q('dialog[open]').count) return; | |
| 367 | 416 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 368 | 417 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 369 | 418 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 370 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 371 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 419 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 423 | })(-1); | |
| 373 | 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 | 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 | 426 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 376 | 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(); | |
| 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()); | |
| 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(); | |
| 428 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 431 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 381 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 382 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 383 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 384 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 385 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 386 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 387 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 388 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 389 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 390 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 432 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 433 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 434 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 435 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 436 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 437 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 438 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 439 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 440 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 441 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 391 | 442 | k-next from:body -> { | |
| 392 | 443 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 393 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>SHA7 modify a file and make a script executable · diffs · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,30 +39,49 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <header class="show"> | |
| 63 | 86 | <h1>modify a file and make a script executable</h1> | |
| 64 | 87 | <pre class="msg">The body has a second paragraph. | |
| @@ -73,7 +96,9 @@ And <b>html</b> & "quotes".</pre> | |||
| 73 | 96 | <a href="/diffs/commit/SHA40">SHA7</a> | |
| 74 | 97 | </dd> | |
| 75 | 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 | 102 | </dl> | |
| 78 | 103 | </header> | |
| 79 | 104 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | |
| @@ -82,9 +107,12 @@ And <b>html</b> & "quotes".</pre> | |||
| 82 | 107 | </span> | |
| 83 | 108 | <span class="grow"> | |
| 84 | 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> | |
| 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> | |
| 87 | - | <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=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> | |
| 111 | + | </a> | |
| 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 | 116 | </div> | |
| 89 | 117 | <div id="diffs" data-diff="split"> | |
| 90 | 118 | <details class="dfile" id="f1" data-file open> | |
| @@ -232,10 +260,8 @@ And <b>html</b> & "quotes".</pre> | |||
| 232 | 260 | </div> | |
| 233 | 261 | </main> | |
| 234 | 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 | 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 | 265 | </h2> | |
| 240 | 266 | <ol class="files" aria-label="Changed files"> | |
| 241 | 267 | <li> | |
| @@ -269,73 +295,73 @@ And <b>html</b> & "quotes".</pre> | |||
| 269 | 295 | </li> | |
| 270 | 296 | </ol> | |
| 271 | 297 | </section> | |
| 272 | - | </details> | |
| 273 | 298 | </aside> | |
| 274 | 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 | 315 | <div class="panel-head"> | |
| 277 | - | <span>keys</span> | |
| 278 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 316 | + | <h2>Keyboard shortcuts</h2> | |
| 317 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 318 | + | <kbd>esc</kbd> close</button> | |
| 279 | 319 | </div> | |
| 280 | 320 | <dl class="keys"> | |
| 281 | 321 | <div> | |
| 282 | 322 | <dt> | |
| 283 | - | <kbd>j</kbd> | |
| 284 | - | <kbd>k</kbd> | |
| 323 | + | <kbd>ctrl k</kbd> | |
| 324 | + | <kbd>:</kbd> | |
| 285 | 325 | </dt> | |
| 286 | - | <dd>move cursor down / up</dd> | |
| 326 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 287 | 327 | </div> | |
| 288 | 328 | <div> | |
| 289 | 329 | <dt> | |
| 290 | - | <kbd>enter</kbd> | |
| 330 | + | <kbd>?</kbd> | |
| 291 | 331 | </dt> | |
| 292 | - | <dd>open row under cursor</dd> | |
| 332 | + | <dd>this list</dd> | |
| 293 | 333 | </div> | |
| 294 | 334 | <div> | |
| 295 | 335 | <dt> | |
| 296 | - | <kbd>g</kbd> | |
| 297 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 336 | + | <kbd>j</kbd> | |
| 337 | + | <kbd>k</kbd> | |
| 298 | 338 | </dt> | |
| 299 | - | <dd>first / last row</dd> | |
| 339 | + | <dd>move the row cursor down / up</dd> | |
| 300 | 340 | </div> | |
| 301 | 341 | <div> | |
| 302 | 342 | <dt> | |
| 303 | - | <kbd>/</kbd> | |
| 343 | + | <kbd>enter</kbd> | |
| 304 | 344 | </dt> | |
| 305 | - | <dd>focus the filter</dd> | |
| 345 | + | <dd>open the row under the cursor</dd> | |
| 306 | 346 | </div> | |
| 307 | 347 | <div> | |
| 308 | 348 | <dt> | |
| 309 | - | <kbd>g h</kbd> | |
| 310 | - | <kbd>g t</kbd> | |
| 349 | + | <kbd>g g</kbd> | |
| 350 | + | <kbd>G</kbd> | |
| 311 | 351 | </dt> | |
| 312 | - | <dd>go home / tree</dd> | |
| 352 | + | <dd>first / last row</dd> | |
| 313 | 353 | </div> | |
| 314 | 354 | <div> | |
| 315 | 355 | <dt> | |
| 316 | - | <kbd>g c</kbd> | |
| 317 | - | <kbd>g r</kbd> | |
| 356 | + | <kbd>/</kbd> | |
| 318 | 357 | </dt> | |
| 319 | - | <dd>go commits / refs</dd> | |
| 358 | + | <dd>filter the list</dd> | |
| 320 | 359 | </div> | |
| 321 | 360 | <div> | |
| 322 | 361 | <dt> | |
| 323 | 362 | <kbd>b</kbd> | |
| 324 | 363 | </dt> | |
| 325 | - | <dd>switch ref (branches, tags)</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> | |
| 364 | + | <dd>switch branch or tag</dd> | |
| 339 | 365 | </div> | |
| 340 | 366 | <div> | |
| 341 | 367 | <dt> | |
| @@ -343,15 +369,21 @@ And <b>html</b> & "quotes".</pre> | |||
| 343 | 369 | <kbd>t</kbd> | |
| 344 | 370 | <kbd>c</kbd> | |
| 345 | 371 | <kbd>r</kbd> | |
| 372 | + | <kbd>m</kbd> | |
| 346 | 373 | </dt> | |
| 347 | - | <dd>repo sections (when shown)</dd> | |
| 374 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 348 | 375 | </div> | |
| 349 | 376 | <div> | |
| 350 | 377 | <dt> | |
| 351 | - | <kbd>m</kbd> | |
| 352 | - | <kbd>g m</kbd> | |
| 378 | + | <kbd>g h</kbd> | |
| 379 | + | </dt> | |
| 380 | + | <dd>home</dd> | |
| 381 | + | </div> | |
| 382 | + | <div> | |
| 383 | + | <dt> | |
| 384 | + | <kbd>y</kbd> | |
| 353 | 385 | </dt> | |
| 354 | - | <dd>merge requests (when shown)</dd> | |
| 386 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 355 | 387 | </div> | |
| 356 | 388 | <div> | |
| 357 | 389 | <dt> | |
| @@ -371,19 +403,21 @@ And <b>html</b> & "quotes".</pre> | |||
| 371 | 403 | <dt> | |
| 372 | 404 | <kbd>esc</kbd> | |
| 373 | 405 | </dt> | |
| 374 | - | <dd>close panel</dd> | |
| 406 | + | <dd>close a dialog, leave a field</dd> | |
| 375 | 407 | </div> | |
| 376 | 408 | </dl> | |
| 377 | 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 | 411 | <div class="panel-head"> | |
| 380 | - | <label for="refs-filter">ref></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))"> | |
| 382 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 412 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 414 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 415 | + | <kbd>esc</kbd> | |
| 416 | + | </button> | |
| 383 | 417 | </div> | |
| 384 | 418 | <div class="panel-cols" data-picklist> | |
| 385 | 419 | <section aria-label="Branches"> | |
| 386 | - | <h2>branches <span class="mute">2</span> | |
| 420 | + | <h2>Branches <span class="count">2</span> | |
| 387 | 421 | </h2> | |
| 388 | 422 | <ul> | |
| 389 | 423 | <li> | |
| @@ -397,7 +431,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 397 | 431 | </ul> | |
| 398 | 432 | </section> | |
| 399 | 433 | <section aria-label="Tags"> | |
| 400 | - | <h2>tags <span class="mute">1</span> | |
| 434 | + | <h2>Tags <span class="count">1</span> | |
| 401 | 435 | </h2> | |
| 402 | 436 | <ul> | |
| 403 | 437 | <li> | |
| @@ -408,105 +442,124 @@ And <b>html</b> & "quotes".</pre> | |||
| 408 | 442 | </section> | |
| 409 | 443 | </div> | |
| 410 | 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 | 446 | <div class="panel-head"> | |
| 413 | - | <label for="cmd-q" class="hi">:</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(); | |
| 447 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 449 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 416 | 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 | 451 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 418 | 452 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 419 | 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 | 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 | 458 | </div> | |
| 423 | - | <ul id="cmd-list" class="cmd-list"> | |
| 459 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 424 | 460 | <li data-k="go home" data-cursor> | |
| 425 | 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 | 465 | </li> | |
| 428 | 466 | <li data-k="section summary"> | |
| 429 | 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 | 471 | </li> | |
| 432 | - | <li data-k="section tree"> | |
| 472 | + | <li data-k="section files"> | |
| 433 | 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 | 477 | </li> | |
| 436 | 478 | <li data-k="section commits"> | |
| 437 | 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 | 483 | </li> | |
| 440 | - | <li data-k="section refs"> | |
| 484 | + | <li data-k="section branches & tags"> | |
| 441 | 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 | 489 | </li> | |
| 444 | 490 | <li data-k="section merge requests"> | |
| 445 | 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 | 495 | </li> | |
| 448 | 496 | <li data-k="branch main"> | |
| 449 | 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 | 501 | </li> | |
| 452 | 502 | <li data-k="branch side"> | |
| 453 | 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 | 507 | </li> | |
| 456 | 508 | <li data-k="tag v1.0.0"> | |
| 457 | 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 | 513 | </li> | |
| 460 | 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 | 524 | </dialog> | |
| 462 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 525 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 477 | 526 | if (q('dialog[open]').count) return; | |
| 478 | 527 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 479 | 528 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 480 | 529 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 481 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 482 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 530 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 534 | })(1); | |
| 484 | - | k-k from:body -> ((d) => { | |
| 535 | + | k-k from:body -> (async (d) => { | |
| 485 | 536 | if (q('dialog[open]').count) return; | |
| 486 | 537 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 487 | 538 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 488 | 539 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 489 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 490 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 540 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 544 | })(-1); | |
| 492 | 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 | 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 | 547 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 495 | 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(); | |
| 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()); | |
| 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(); | |
| 549 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 552 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 500 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 501 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 502 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 503 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 504 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 505 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 506 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 507 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 508 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 509 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 553 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 554 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 555 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 556 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 557 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 558 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 559 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 560 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 561 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 562 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 510 | 563 | k-next from:body -> { | |
| 511 | 564 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 512 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>SHA7 modify a file and make a script executable · diffs · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,30 +39,49 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <header class="show"> | |
| 63 | 86 | <h1>modify a file and make a script executable</h1> | |
| 64 | 87 | <pre class="msg">The body has a second paragraph. | |
| @@ -73,7 +96,9 @@ And <b>html</b> & "quotes".</pre> | |||
| 73 | 96 | <a href="/diffs/commit/SHA40">SHA7</a> | |
| 74 | 97 | </dd> | |
| 75 | 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 | 102 | </dl> | |
| 78 | 103 | </header> | |
| 79 | 104 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | |
| @@ -82,9 +107,12 @@ And <b>html</b> & "quotes".</pre> | |||
| 82 | 107 | </span> | |
| 83 | 108 | <span class="grow"> | |
| 84 | 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> | |
| 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> | |
| 87 | - | <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=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> | |
| 111 | + | </a> | |
| 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 | 116 | </div> | |
| 89 | 117 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 90 | 118 | <details class="dfile" id="f1" data-file open> | |
| @@ -232,10 +260,8 @@ And <b>html</b> & "quotes".</pre> | |||
| 232 | 260 | </div> | |
| 233 | 261 | </main> | |
| 234 | 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 | 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 | 265 | </h2> | |
| 240 | 266 | <ol class="files" aria-label="Changed files"> | |
| 241 | 267 | <li> | |
| @@ -269,73 +295,73 @@ And <b>html</b> & "quotes".</pre> | |||
| 269 | 295 | </li> | |
| 270 | 296 | </ol> | |
| 271 | 297 | </section> | |
| 272 | - | </details> | |
| 273 | 298 | </aside> | |
| 274 | 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 | 315 | <div class="panel-head"> | |
| 277 | - | <span>keys</span> | |
| 278 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 316 | + | <h2>Keyboard shortcuts</h2> | |
| 317 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 318 | + | <kbd>esc</kbd> close</button> | |
| 279 | 319 | </div> | |
| 280 | 320 | <dl class="keys"> | |
| 281 | 321 | <div> | |
| 282 | 322 | <dt> | |
| 283 | - | <kbd>j</kbd> | |
| 284 | - | <kbd>k</kbd> | |
| 323 | + | <kbd>ctrl k</kbd> | |
| 324 | + | <kbd>:</kbd> | |
| 285 | 325 | </dt> | |
| 286 | - | <dd>move cursor down / up</dd> | |
| 326 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 287 | 327 | </div> | |
| 288 | 328 | <div> | |
| 289 | 329 | <dt> | |
| 290 | - | <kbd>enter</kbd> | |
| 330 | + | <kbd>?</kbd> | |
| 291 | 331 | </dt> | |
| 292 | - | <dd>open row under cursor</dd> | |
| 332 | + | <dd>this list</dd> | |
| 293 | 333 | </div> | |
| 294 | 334 | <div> | |
| 295 | 335 | <dt> | |
| 296 | - | <kbd>g</kbd> | |
| 297 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 336 | + | <kbd>j</kbd> | |
| 337 | + | <kbd>k</kbd> | |
| 298 | 338 | </dt> | |
| 299 | - | <dd>first / last row</dd> | |
| 339 | + | <dd>move the row cursor down / up</dd> | |
| 300 | 340 | </div> | |
| 301 | 341 | <div> | |
| 302 | 342 | <dt> | |
| 303 | - | <kbd>/</kbd> | |
| 343 | + | <kbd>enter</kbd> | |
| 304 | 344 | </dt> | |
| 305 | - | <dd>focus the filter</dd> | |
| 345 | + | <dd>open the row under the cursor</dd> | |
| 306 | 346 | </div> | |
| 307 | 347 | <div> | |
| 308 | 348 | <dt> | |
| 309 | - | <kbd>g h</kbd> | |
| 310 | - | <kbd>g t</kbd> | |
| 349 | + | <kbd>g g</kbd> | |
| 350 | + | <kbd>G</kbd> | |
| 311 | 351 | </dt> | |
| 312 | - | <dd>go home / tree</dd> | |
| 352 | + | <dd>first / last row</dd> | |
| 313 | 353 | </div> | |
| 314 | 354 | <div> | |
| 315 | 355 | <dt> | |
| 316 | - | <kbd>g c</kbd> | |
| 317 | - | <kbd>g r</kbd> | |
| 356 | + | <kbd>/</kbd> | |
| 318 | 357 | </dt> | |
| 319 | - | <dd>go commits / refs</dd> | |
| 358 | + | <dd>filter the list</dd> | |
| 320 | 359 | </div> | |
| 321 | 360 | <div> | |
| 322 | 361 | <dt> | |
| 323 | 362 | <kbd>b</kbd> | |
| 324 | 363 | </dt> | |
| 325 | - | <dd>switch ref (branches, tags)</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> | |
| 364 | + | <dd>switch branch or tag</dd> | |
| 339 | 365 | </div> | |
| 340 | 366 | <div> | |
| 341 | 367 | <dt> | |
| @@ -343,15 +369,21 @@ And <b>html</b> & "quotes".</pre> | |||
| 343 | 369 | <kbd>t</kbd> | |
| 344 | 370 | <kbd>c</kbd> | |
| 345 | 371 | <kbd>r</kbd> | |
| 372 | + | <kbd>m</kbd> | |
| 346 | 373 | </dt> | |
| 347 | - | <dd>repo sections (when shown)</dd> | |
| 374 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 348 | 375 | </div> | |
| 349 | 376 | <div> | |
| 350 | 377 | <dt> | |
| 351 | - | <kbd>m</kbd> | |
| 352 | - | <kbd>g m</kbd> | |
| 378 | + | <kbd>g h</kbd> | |
| 379 | + | </dt> | |
| 380 | + | <dd>home</dd> | |
| 381 | + | </div> | |
| 382 | + | <div> | |
| 383 | + | <dt> | |
| 384 | + | <kbd>y</kbd> | |
| 353 | 385 | </dt> | |
| 354 | - | <dd>merge requests (when shown)</dd> | |
| 386 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 355 | 387 | </div> | |
| 356 | 388 | <div> | |
| 357 | 389 | <dt> | |
| @@ -371,19 +403,21 @@ And <b>html</b> & "quotes".</pre> | |||
| 371 | 403 | <dt> | |
| 372 | 404 | <kbd>esc</kbd> | |
| 373 | 405 | </dt> | |
| 374 | - | <dd>close panel</dd> | |
| 406 | + | <dd>close a dialog, leave a field</dd> | |
| 375 | 407 | </div> | |
| 376 | 408 | </dl> | |
| 377 | 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 | 411 | <div class="panel-head"> | |
| 380 | - | <label for="refs-filter">ref></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))"> | |
| 382 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 412 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 414 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 415 | + | <kbd>esc</kbd> | |
| 416 | + | </button> | |
| 383 | 417 | </div> | |
| 384 | 418 | <div class="panel-cols" data-picklist> | |
| 385 | 419 | <section aria-label="Branches"> | |
| 386 | - | <h2>branches <span class="mute">2</span> | |
| 420 | + | <h2>Branches <span class="count">2</span> | |
| 387 | 421 | </h2> | |
| 388 | 422 | <ul> | |
| 389 | 423 | <li> | |
| @@ -397,7 +431,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 397 | 431 | </ul> | |
| 398 | 432 | </section> | |
| 399 | 433 | <section aria-label="Tags"> | |
| 400 | - | <h2>tags <span class="mute">1</span> | |
| 434 | + | <h2>Tags <span class="count">1</span> | |
| 401 | 435 | </h2> | |
| 402 | 436 | <ul> | |
| 403 | 437 | <li> | |
| @@ -408,105 +442,124 @@ And <b>html</b> & "quotes".</pre> | |||
| 408 | 442 | </section> | |
| 409 | 443 | </div> | |
| 410 | 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 | 446 | <div class="panel-head"> | |
| 413 | - | <label for="cmd-q" class="hi">:</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(); | |
| 447 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 449 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 416 | 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 | 451 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 418 | 452 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 419 | 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 | 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 | 458 | </div> | |
| 423 | - | <ul id="cmd-list" class="cmd-list"> | |
| 459 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 424 | 460 | <li data-k="go home" data-cursor> | |
| 425 | 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 | 465 | </li> | |
| 428 | 466 | <li data-k="section summary"> | |
| 429 | 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 | 471 | </li> | |
| 432 | - | <li data-k="section tree"> | |
| 472 | + | <li data-k="section files"> | |
| 433 | 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 | 477 | </li> | |
| 436 | 478 | <li data-k="section commits"> | |
| 437 | 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 | 483 | </li> | |
| 440 | - | <li data-k="section refs"> | |
| 484 | + | <li data-k="section branches & tags"> | |
| 441 | 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 | 489 | </li> | |
| 444 | 490 | <li data-k="section merge requests"> | |
| 445 | 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 | 495 | </li> | |
| 448 | 496 | <li data-k="branch main"> | |
| 449 | 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 | 501 | </li> | |
| 452 | 502 | <li data-k="branch side"> | |
| 453 | 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 | 507 | </li> | |
| 456 | 508 | <li data-k="tag v1.0.0"> | |
| 457 | 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 | 513 | </li> | |
| 460 | 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 | 524 | </dialog> | |
| 462 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 525 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 477 | 526 | if (q('dialog[open]').count) return; | |
| 478 | 527 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 479 | 528 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 480 | 529 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 481 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 482 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 530 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 534 | })(1); | |
| 484 | - | k-k from:body -> ((d) => { | |
| 535 | + | k-k from:body -> (async (d) => { | |
| 485 | 536 | if (q('dialog[open]').count) return; | |
| 486 | 537 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 487 | 538 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 488 | 539 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 489 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 490 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 540 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 544 | })(-1); | |
| 492 | 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 | 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 | 547 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 495 | 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(); | |
| 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()); | |
| 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(); | |
| 549 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 552 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 500 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 501 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 502 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 503 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 504 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 505 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 506 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 507 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 508 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 509 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 553 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 554 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 555 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 556 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 557 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 558 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 559 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 560 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 561 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 562 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 510 | 563 | k-next from:body -> { | |
| 511 | 564 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 512 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>SHA7 rename a file and delete another · diffs · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,30 +39,49 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <header class="show"> | |
| 63 | 86 | <h1>rename a file and delete another</h1> | |
| 64 | 87 | <dl class="kv show-kv"> | |
| @@ -78,9 +101,12 @@ | |||
| 78 | 101 | </span> | |
| 79 | 102 | <span class="grow"> | |
| 80 | 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> | |
| 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> | |
| 83 | - | <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=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> | |
| 105 | + | </a> | |
| 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 | 110 | </div> | |
| 85 | 111 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 86 | 112 | <details class="dfile" id="f1" data-file> | |
| @@ -138,10 +164,8 @@ | |||
| 138 | 164 | </div> | |
| 139 | 165 | </main> | |
| 140 | 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 | 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 | 169 | </h2> | |
| 146 | 170 | <ol class="files" aria-label="Changed files"> | |
| 147 | 171 | <li> | |
| @@ -176,73 +200,73 @@ | |||
| 176 | 200 | </li> | |
| 177 | 201 | </ol> | |
| 178 | 202 | </section> | |
| 179 | - | </details> | |
| 180 | 203 | </aside> | |
| 181 | 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 | 220 | <div class="panel-head"> | |
| 184 | - | <span>keys</span> | |
| 185 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 221 | + | <h2>Keyboard shortcuts</h2> | |
| 222 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 223 | + | <kbd>esc</kbd> close</button> | |
| 186 | 224 | </div> | |
| 187 | 225 | <dl class="keys"> | |
| 188 | 226 | <div> | |
| 189 | 227 | <dt> | |
| 190 | - | <kbd>j</kbd> | |
| 191 | - | <kbd>k</kbd> | |
| 228 | + | <kbd>ctrl k</kbd> | |
| 229 | + | <kbd>:</kbd> | |
| 192 | 230 | </dt> | |
| 193 | - | <dd>move cursor down / up</dd> | |
| 231 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 194 | 232 | </div> | |
| 195 | 233 | <div> | |
| 196 | 234 | <dt> | |
| 197 | - | <kbd>enter</kbd> | |
| 235 | + | <kbd>?</kbd> | |
| 198 | 236 | </dt> | |
| 199 | - | <dd>open row under cursor</dd> | |
| 237 | + | <dd>this list</dd> | |
| 200 | 238 | </div> | |
| 201 | 239 | <div> | |
| 202 | 240 | <dt> | |
| 203 | - | <kbd>g</kbd> | |
| 204 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 241 | + | <kbd>j</kbd> | |
| 242 | + | <kbd>k</kbd> | |
| 205 | 243 | </dt> | |
| 206 | - | <dd>first / last row</dd> | |
| 244 | + | <dd>move the row cursor down / up</dd> | |
| 207 | 245 | </div> | |
| 208 | 246 | <div> | |
| 209 | 247 | <dt> | |
| 210 | - | <kbd>/</kbd> | |
| 248 | + | <kbd>enter</kbd> | |
| 211 | 249 | </dt> | |
| 212 | - | <dd>focus the filter</dd> | |
| 250 | + | <dd>open the row under the cursor</dd> | |
| 213 | 251 | </div> | |
| 214 | 252 | <div> | |
| 215 | 253 | <dt> | |
| 216 | - | <kbd>g h</kbd> | |
| 217 | - | <kbd>g t</kbd> | |
| 254 | + | <kbd>g g</kbd> | |
| 255 | + | <kbd>G</kbd> | |
| 218 | 256 | </dt> | |
| 219 | - | <dd>go home / tree</dd> | |
| 257 | + | <dd>first / last row</dd> | |
| 220 | 258 | </div> | |
| 221 | 259 | <div> | |
| 222 | 260 | <dt> | |
| 223 | - | <kbd>g c</kbd> | |
| 224 | - | <kbd>g r</kbd> | |
| 261 | + | <kbd>/</kbd> | |
| 225 | 262 | </dt> | |
| 226 | - | <dd>go commits / refs</dd> | |
| 263 | + | <dd>filter the list</dd> | |
| 227 | 264 | </div> | |
| 228 | 265 | <div> | |
| 229 | 266 | <dt> | |
| 230 | 267 | <kbd>b</kbd> | |
| 231 | 268 | </dt> | |
| 232 | - | <dd>switch ref (branches, tags)</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> | |
| 269 | + | <dd>switch branch or tag</dd> | |
| 246 | 270 | </div> | |
| 247 | 271 | <div> | |
| 248 | 272 | <dt> | |
| @@ -250,15 +274,21 @@ | |||
| 250 | 274 | <kbd>t</kbd> | |
| 251 | 275 | <kbd>c</kbd> | |
| 252 | 276 | <kbd>r</kbd> | |
| 277 | + | <kbd>m</kbd> | |
| 253 | 278 | </dt> | |
| 254 | - | <dd>repo sections (when shown)</dd> | |
| 279 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 255 | 280 | </div> | |
| 256 | 281 | <div> | |
| 257 | 282 | <dt> | |
| 258 | - | <kbd>m</kbd> | |
| 259 | - | <kbd>g m</kbd> | |
| 283 | + | <kbd>g h</kbd> | |
| 284 | + | </dt> | |
| 285 | + | <dd>home</dd> | |
| 286 | + | </div> | |
| 287 | + | <div> | |
| 288 | + | <dt> | |
| 289 | + | <kbd>y</kbd> | |
| 260 | 290 | </dt> | |
| 261 | - | <dd>merge requests (when shown)</dd> | |
| 291 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 262 | 292 | </div> | |
| 263 | 293 | <div> | |
| 264 | 294 | <dt> | |
| @@ -278,19 +308,21 @@ | |||
| 278 | 308 | <dt> | |
| 279 | 309 | <kbd>esc</kbd> | |
| 280 | 310 | </dt> | |
| 281 | - | <dd>close panel</dd> | |
| 311 | + | <dd>close a dialog, leave a field</dd> | |
| 282 | 312 | </div> | |
| 283 | 313 | </dl> | |
| 284 | 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 | 316 | <div class="panel-head"> | |
| 287 | - | <label for="refs-filter">ref></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))"> | |
| 289 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 317 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 319 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 320 | + | <kbd>esc</kbd> | |
| 321 | + | </button> | |
| 290 | 322 | </div> | |
| 291 | 323 | <div class="panel-cols" data-picklist> | |
| 292 | 324 | <section aria-label="Branches"> | |
| 293 | - | <h2>branches <span class="mute">2</span> | |
| 325 | + | <h2>Branches <span class="count">2</span> | |
| 294 | 326 | </h2> | |
| 295 | 327 | <ul> | |
| 296 | 328 | <li> | |
| @@ -304,7 +336,7 @@ | |||
| 304 | 336 | </ul> | |
| 305 | 337 | </section> | |
| 306 | 338 | <section aria-label="Tags"> | |
| 307 | - | <h2>tags <span class="mute">1</span> | |
| 339 | + | <h2>Tags <span class="count">1</span> | |
| 308 | 340 | </h2> | |
| 309 | 341 | <ul> | |
| 310 | 342 | <li> | |
| @@ -315,105 +347,124 @@ | |||
| 315 | 347 | </section> | |
| 316 | 348 | </div> | |
| 317 | 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 | 351 | <div class="panel-head"> | |
| 320 | - | <label for="cmd-q" class="hi">:</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(); | |
| 352 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 354 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 323 | 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 | 356 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 325 | 357 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 326 | 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 | 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 | 363 | </div> | |
| 330 | - | <ul id="cmd-list" class="cmd-list"> | |
| 364 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 331 | 365 | <li data-k="go home" data-cursor> | |
| 332 | 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 | 370 | </li> | |
| 335 | 371 | <li data-k="section summary"> | |
| 336 | 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 | 376 | </li> | |
| 339 | - | <li data-k="section tree"> | |
| 377 | + | <li data-k="section files"> | |
| 340 | 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 | 382 | </li> | |
| 343 | 383 | <li data-k="section commits"> | |
| 344 | 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 | 388 | </li> | |
| 347 | - | <li data-k="section refs"> | |
| 389 | + | <li data-k="section branches & tags"> | |
| 348 | 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 | 394 | </li> | |
| 351 | 395 | <li data-k="section merge requests"> | |
| 352 | 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 | 400 | </li> | |
| 355 | 401 | <li data-k="branch main"> | |
| 356 | 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 | 406 | </li> | |
| 359 | 407 | <li data-k="branch side"> | |
| 360 | 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 | 412 | </li> | |
| 363 | 413 | <li data-k="tag v1.0.0"> | |
| 364 | 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 | 418 | </li> | |
| 367 | 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 | 429 | </dialog> | |
| 369 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 430 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 384 | 431 | if (q('dialog[open]').count) return; | |
| 385 | 432 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 386 | 433 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 387 | 434 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 388 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 389 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 435 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 439 | })(1); | |
| 391 | - | k-k from:body -> ((d) => { | |
| 440 | + | k-k from:body -> (async (d) => { | |
| 392 | 441 | if (q('dialog[open]').count) return; | |
| 393 | 442 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 394 | 443 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 395 | 444 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 396 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 397 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 445 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 449 | })(-1); | |
| 399 | 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 | 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 | 452 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 402 | 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(); | |
| 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()); | |
| 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(); | |
| 454 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 457 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 407 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 408 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 409 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 410 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 411 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 412 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 413 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 414 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 415 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 416 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 458 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 459 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 460 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 461 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 462 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 463 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 464 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 465 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 466 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 467 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 417 | 468 | k-next from:body -> { | |
| 418 | 469 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 419 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>SHA7 root: the first files · diffs · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,30 +39,49 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <header class="show"> | |
| 63 | 86 | <h1>root: the first files</h1> | |
| 64 | 87 | <dl class="kv show-kv"> | |
| @@ -76,9 +99,12 @@ | |||
| 76 | 99 | </span> | |
| 77 | 100 | <span class="grow"> | |
| 78 | 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> | |
| 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> | |
| 81 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | |
| 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> | |
| 103 | + | </a> | |
| 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 | 108 | </div> | |
| 83 | 109 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 84 | 110 | <details class="dfile" id="f1" data-file open> | |
| @@ -388,10 +414,8 @@ | |||
| 388 | 414 | </div> | |
| 389 | 415 | </main> | |
| 390 | 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 | 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 | 419 | </h2> | |
| 396 | 420 | <ol class="files" aria-label="Changed files"> | |
| 397 | 421 | <li> | |
| @@ -468,73 +492,73 @@ | |||
| 468 | 492 | </li> | |
| 469 | 493 | </ol> | |
| 470 | 494 | </section> | |
| 471 | - | </details> | |
| 472 | 495 | </aside> | |
| 473 | 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 | 512 | <div class="panel-head"> | |
| 476 | - | <span>keys</span> | |
| 477 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 513 | + | <h2>Keyboard shortcuts</h2> | |
| 514 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 515 | + | <kbd>esc</kbd> close</button> | |
| 478 | 516 | </div> | |
| 479 | 517 | <dl class="keys"> | |
| 480 | 518 | <div> | |
| 481 | 519 | <dt> | |
| 482 | - | <kbd>j</kbd> | |
| 483 | - | <kbd>k</kbd> | |
| 520 | + | <kbd>ctrl k</kbd> | |
| 521 | + | <kbd>:</kbd> | |
| 484 | 522 | </dt> | |
| 485 | - | <dd>move cursor down / up</dd> | |
| 523 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 486 | 524 | </div> | |
| 487 | 525 | <div> | |
| 488 | 526 | <dt> | |
| 489 | - | <kbd>enter</kbd> | |
| 527 | + | <kbd>?</kbd> | |
| 490 | 528 | </dt> | |
| 491 | - | <dd>open row under cursor</dd> | |
| 529 | + | <dd>this list</dd> | |
| 492 | 530 | </div> | |
| 493 | 531 | <div> | |
| 494 | 532 | <dt> | |
| 495 | - | <kbd>g</kbd> | |
| 496 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 533 | + | <kbd>j</kbd> | |
| 534 | + | <kbd>k</kbd> | |
| 497 | 535 | </dt> | |
| 498 | - | <dd>first / last row</dd> | |
| 536 | + | <dd>move the row cursor down / up</dd> | |
| 499 | 537 | </div> | |
| 500 | 538 | <div> | |
| 501 | 539 | <dt> | |
| 502 | - | <kbd>/</kbd> | |
| 540 | + | <kbd>enter</kbd> | |
| 503 | 541 | </dt> | |
| 504 | - | <dd>focus the filter</dd> | |
| 542 | + | <dd>open the row under the cursor</dd> | |
| 505 | 543 | </div> | |
| 506 | 544 | <div> | |
| 507 | 545 | <dt> | |
| 508 | - | <kbd>g h</kbd> | |
| 509 | - | <kbd>g t</kbd> | |
| 546 | + | <kbd>g g</kbd> | |
| 547 | + | <kbd>G</kbd> | |
| 510 | 548 | </dt> | |
| 511 | - | <dd>go home / tree</dd> | |
| 549 | + | <dd>first / last row</dd> | |
| 512 | 550 | </div> | |
| 513 | 551 | <div> | |
| 514 | 552 | <dt> | |
| 515 | - | <kbd>g c</kbd> | |
| 516 | - | <kbd>g r</kbd> | |
| 553 | + | <kbd>/</kbd> | |
| 517 | 554 | </dt> | |
| 518 | - | <dd>go commits / refs</dd> | |
| 555 | + | <dd>filter the list</dd> | |
| 519 | 556 | </div> | |
| 520 | 557 | <div> | |
| 521 | 558 | <dt> | |
| 522 | 559 | <kbd>b</kbd> | |
| 523 | 560 | </dt> | |
| 524 | - | <dd>switch ref (branches, tags)</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> | |
| 561 | + | <dd>switch branch or tag</dd> | |
| 538 | 562 | </div> | |
| 539 | 563 | <div> | |
| 540 | 564 | <dt> | |
| @@ -542,15 +566,21 @@ | |||
| 542 | 566 | <kbd>t</kbd> | |
| 543 | 567 | <kbd>c</kbd> | |
| 544 | 568 | <kbd>r</kbd> | |
| 569 | + | <kbd>m</kbd> | |
| 545 | 570 | </dt> | |
| 546 | - | <dd>repo sections (when shown)</dd> | |
| 571 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 547 | 572 | </div> | |
| 548 | 573 | <div> | |
| 549 | 574 | <dt> | |
| 550 | - | <kbd>m</kbd> | |
| 551 | - | <kbd>g m</kbd> | |
| 575 | + | <kbd>g h</kbd> | |
| 576 | + | </dt> | |
| 577 | + | <dd>home</dd> | |
| 578 | + | </div> | |
| 579 | + | <div> | |
| 580 | + | <dt> | |
| 581 | + | <kbd>y</kbd> | |
| 552 | 582 | </dt> | |
| 553 | - | <dd>merge requests (when shown)</dd> | |
| 583 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 554 | 584 | </div> | |
| 555 | 585 | <div> | |
| 556 | 586 | <dt> | |
| @@ -570,19 +600,21 @@ | |||
| 570 | 600 | <dt> | |
| 571 | 601 | <kbd>esc</kbd> | |
| 572 | 602 | </dt> | |
| 573 | - | <dd>close panel</dd> | |
| 603 | + | <dd>close a dialog, leave a field</dd> | |
| 574 | 604 | </div> | |
| 575 | 605 | </dl> | |
| 576 | 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 | 608 | <div class="panel-head"> | |
| 579 | - | <label for="refs-filter">ref></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))"> | |
| 581 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 609 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 611 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 612 | + | <kbd>esc</kbd> | |
| 613 | + | </button> | |
| 582 | 614 | </div> | |
| 583 | 615 | <div class="panel-cols" data-picklist> | |
| 584 | 616 | <section aria-label="Branches"> | |
| 585 | - | <h2>branches <span class="mute">2</span> | |
| 617 | + | <h2>Branches <span class="count">2</span> | |
| 586 | 618 | </h2> | |
| 587 | 619 | <ul> | |
| 588 | 620 | <li> | |
| @@ -596,7 +628,7 @@ | |||
| 596 | 628 | </ul> | |
| 597 | 629 | </section> | |
| 598 | 630 | <section aria-label="Tags"> | |
| 599 | - | <h2>tags <span class="mute">1</span> | |
| 631 | + | <h2>Tags <span class="count">1</span> | |
| 600 | 632 | </h2> | |
| 601 | 633 | <ul> | |
| 602 | 634 | <li> | |
| @@ -607,105 +639,124 @@ | |||
| 607 | 639 | </section> | |
| 608 | 640 | </div> | |
| 609 | 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 | 643 | <div class="panel-head"> | |
| 612 | - | <label for="cmd-q" class="hi">:</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(); | |
| 644 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 646 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 615 | 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 | 648 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 617 | 649 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 618 | 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 | 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 | 655 | </div> | |
| 622 | - | <ul id="cmd-list" class="cmd-list"> | |
| 656 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 623 | 657 | <li data-k="go home" data-cursor> | |
| 624 | 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 | 662 | </li> | |
| 627 | 663 | <li data-k="section summary"> | |
| 628 | 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 | 668 | </li> | |
| 631 | - | <li data-k="section tree"> | |
| 669 | + | <li data-k="section files"> | |
| 632 | 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 | 674 | </li> | |
| 635 | 675 | <li data-k="section commits"> | |
| 636 | 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 | 680 | </li> | |
| 639 | - | <li data-k="section refs"> | |
| 681 | + | <li data-k="section branches & tags"> | |
| 640 | 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 | 686 | </li> | |
| 643 | 687 | <li data-k="section merge requests"> | |
| 644 | 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 | 692 | </li> | |
| 647 | 693 | <li data-k="branch main"> | |
| 648 | 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 | 698 | </li> | |
| 651 | 699 | <li data-k="branch side"> | |
| 652 | 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 | 704 | </li> | |
| 655 | 705 | <li data-k="tag v1.0.0"> | |
| 656 | 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 | 710 | </li> | |
| 659 | 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 | 721 | </dialog> | |
| 661 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 722 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 676 | 723 | if (q('dialog[open]').count) return; | |
| 677 | 724 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 678 | 725 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 679 | 726 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 680 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 681 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 727 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 731 | })(1); | |
| 683 | - | k-k from:body -> ((d) => { | |
| 732 | + | k-k from:body -> (async (d) => { | |
| 684 | 733 | if (q('dialog[open]').count) return; | |
| 685 | 734 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 686 | 735 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 687 | 736 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 688 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 689 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 737 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 741 | })(-1); | |
| 691 | 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 | 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 | 744 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 694 | 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(); | |
| 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()); | |
| 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(); | |
| 746 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 749 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 699 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 700 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 701 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 702 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 703 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 704 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 705 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 706 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 707 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 708 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 750 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 751 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 752 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 753 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 754 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 755 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 756 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 757 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 758 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 759 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 709 | 760 | k-next from:body -> { | |
| 710 | 761 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 711 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>SHA7 add evil.go · diffs · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="SHA40" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="SHA40" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,30 +39,49 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <header class="show"> | |
| 63 | 86 | <h1>add evil.go</h1> | |
| 64 | 87 | <pre class="msg">looks <U+202E>fine<U+202C></pre> | |
| @@ -79,9 +102,12 @@ | |||
| 79 | 102 | </span> | |
| 80 | 103 | <span class="grow"> | |
| 81 | 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> | |
| 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> | |
| 84 | - | <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=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> | |
| 106 | + | </a> | |
| 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 | 111 | </div> | |
| 86 | 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 | 113 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| @@ -262,10 +288,8 @@ | |||
| 262 | 288 | </div> | |
| 263 | 289 | </main> | |
| 264 | 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 | 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 | 293 | </h2> | |
| 270 | 294 | <ol class="files" aria-label="Changed files"> | |
| 271 | 295 | <li> | |
| @@ -285,73 +309,73 @@ | |||
| 285 | 309 | </li> | |
| 286 | 310 | </ol> | |
| 287 | 311 | </section> | |
| 288 | - | </details> | |
| 289 | 312 | </aside> | |
| 290 | 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 | 329 | <div class="panel-head"> | |
| 293 | - | <span>keys</span> | |
| 294 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 330 | + | <h2>Keyboard shortcuts</h2> | |
| 331 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 332 | + | <kbd>esc</kbd> close</button> | |
| 295 | 333 | </div> | |
| 296 | 334 | <dl class="keys"> | |
| 297 | 335 | <div> | |
| 298 | 336 | <dt> | |
| 299 | - | <kbd>j</kbd> | |
| 300 | - | <kbd>k</kbd> | |
| 337 | + | <kbd>ctrl k</kbd> | |
| 338 | + | <kbd>:</kbd> | |
| 301 | 339 | </dt> | |
| 302 | - | <dd>move cursor down / up</dd> | |
| 340 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 303 | 341 | </div> | |
| 304 | 342 | <div> | |
| 305 | 343 | <dt> | |
| 306 | - | <kbd>enter</kbd> | |
| 344 | + | <kbd>?</kbd> | |
| 307 | 345 | </dt> | |
| 308 | - | <dd>open row under cursor</dd> | |
| 346 | + | <dd>this list</dd> | |
| 309 | 347 | </div> | |
| 310 | 348 | <div> | |
| 311 | 349 | <dt> | |
| 312 | - | <kbd>g</kbd> | |
| 313 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 350 | + | <kbd>j</kbd> | |
| 351 | + | <kbd>k</kbd> | |
| 314 | 352 | </dt> | |
| 315 | - | <dd>first / last row</dd> | |
| 353 | + | <dd>move the row cursor down / up</dd> | |
| 316 | 354 | </div> | |
| 317 | 355 | <div> | |
| 318 | 356 | <dt> | |
| 319 | - | <kbd>/</kbd> | |
| 357 | + | <kbd>enter</kbd> | |
| 320 | 358 | </dt> | |
| 321 | - | <dd>focus the filter</dd> | |
| 359 | + | <dd>open the row under the cursor</dd> | |
| 322 | 360 | </div> | |
| 323 | 361 | <div> | |
| 324 | 362 | <dt> | |
| 325 | - | <kbd>g h</kbd> | |
| 326 | - | <kbd>g t</kbd> | |
| 363 | + | <kbd>g g</kbd> | |
| 364 | + | <kbd>G</kbd> | |
| 327 | 365 | </dt> | |
| 328 | - | <dd>go home / tree</dd> | |
| 366 | + | <dd>first / last row</dd> | |
| 329 | 367 | </div> | |
| 330 | 368 | <div> | |
| 331 | 369 | <dt> | |
| 332 | - | <kbd>g c</kbd> | |
| 333 | - | <kbd>g r</kbd> | |
| 370 | + | <kbd>/</kbd> | |
| 334 | 371 | </dt> | |
| 335 | - | <dd>go commits / refs</dd> | |
| 372 | + | <dd>filter the list</dd> | |
| 336 | 373 | </div> | |
| 337 | 374 | <div> | |
| 338 | 375 | <dt> | |
| 339 | 376 | <kbd>b</kbd> | |
| 340 | 377 | </dt> | |
| 341 | - | <dd>switch ref (branches, tags)</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> | |
| 378 | + | <dd>switch branch or tag</dd> | |
| 355 | 379 | </div> | |
| 356 | 380 | <div> | |
| 357 | 381 | <dt> | |
| @@ -359,15 +383,21 @@ | |||
| 359 | 383 | <kbd>t</kbd> | |
| 360 | 384 | <kbd>c</kbd> | |
| 361 | 385 | <kbd>r</kbd> | |
| 386 | + | <kbd>m</kbd> | |
| 362 | 387 | </dt> | |
| 363 | - | <dd>repo sections (when shown)</dd> | |
| 388 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 364 | 389 | </div> | |
| 365 | 390 | <div> | |
| 366 | 391 | <dt> | |
| 367 | - | <kbd>m</kbd> | |
| 368 | - | <kbd>g m</kbd> | |
| 392 | + | <kbd>g h</kbd> | |
| 393 | + | </dt> | |
| 394 | + | <dd>home</dd> | |
| 395 | + | </div> | |
| 396 | + | <div> | |
| 397 | + | <dt> | |
| 398 | + | <kbd>y</kbd> | |
| 369 | 399 | </dt> | |
| 370 | - | <dd>merge requests (when shown)</dd> | |
| 400 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 371 | 401 | </div> | |
| 372 | 402 | <div> | |
| 373 | 403 | <dt> | |
| @@ -387,19 +417,21 @@ | |||
| 387 | 417 | <dt> | |
| 388 | 418 | <kbd>esc</kbd> | |
| 389 | 419 | </dt> | |
| 390 | - | <dd>close panel</dd> | |
| 420 | + | <dd>close a dialog, leave a field</dd> | |
| 391 | 421 | </div> | |
| 392 | 422 | </dl> | |
| 393 | 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 | 425 | <div class="panel-head"> | |
| 396 | - | <label for="refs-filter">ref></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))"> | |
| 398 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 426 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 428 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 429 | + | <kbd>esc</kbd> | |
| 430 | + | </button> | |
| 399 | 431 | </div> | |
| 400 | 432 | <div class="panel-cols" data-picklist> | |
| 401 | 433 | <section aria-label="Branches"> | |
| 402 | - | <h2>branches <span class="mute">2</span> | |
| 434 | + | <h2>Branches <span class="count">2</span> | |
| 403 | 435 | </h2> | |
| 404 | 436 | <ul> | |
| 405 | 437 | <li> | |
| @@ -413,7 +445,7 @@ | |||
| 413 | 445 | </ul> | |
| 414 | 446 | </section> | |
| 415 | 447 | <section aria-label="Tags"> | |
| 416 | - | <h2>tags <span class="mute">1</span> | |
| 448 | + | <h2>Tags <span class="count">1</span> | |
| 417 | 449 | </h2> | |
| 418 | 450 | <ul> | |
| 419 | 451 | <li> | |
| @@ -424,105 +456,124 @@ | |||
| 424 | 456 | </section> | |
| 425 | 457 | </div> | |
| 426 | 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 | 460 | <div class="panel-head"> | |
| 429 | - | <label for="cmd-q" class="hi">:</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(); | |
| 461 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 463 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 432 | 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 | 465 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 434 | 466 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 435 | 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 | 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 | 472 | </div> | |
| 439 | - | <ul id="cmd-list" class="cmd-list"> | |
| 473 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 440 | 474 | <li data-k="go home" data-cursor> | |
| 441 | 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 | 479 | </li> | |
| 444 | 480 | <li data-k="section summary"> | |
| 445 | 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 | 485 | </li> | |
| 448 | - | <li data-k="section tree"> | |
| 486 | + | <li data-k="section files"> | |
| 449 | 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 | 491 | </li> | |
| 452 | 492 | <li data-k="section commits"> | |
| 453 | 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 | 497 | </li> | |
| 456 | - | <li data-k="section refs"> | |
| 498 | + | <li data-k="section branches & tags"> | |
| 457 | 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 | 503 | </li> | |
| 460 | 504 | <li data-k="section merge requests"> | |
| 461 | 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 | 509 | </li> | |
| 464 | 510 | <li data-k="branch main"> | |
| 465 | 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 | 515 | </li> | |
| 468 | 516 | <li data-k="branch side"> | |
| 469 | 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 | 521 | </li> | |
| 472 | 522 | <li data-k="tag v1.0.0"> | |
| 473 | 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 | 527 | </li> | |
| 476 | 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 | 538 | </dialog> | |
| 478 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 539 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 493 | 540 | if (q('dialog[open]').count) return; | |
| 494 | 541 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 495 | 542 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 496 | 543 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 497 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 498 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 544 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 548 | })(1); | |
| 500 | - | k-k from:body -> ((d) => { | |
| 549 | + | k-k from:body -> (async (d) => { | |
| 501 | 550 | if (q('dialog[open]').count) return; | |
| 502 | 551 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 503 | 552 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 504 | 553 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 505 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 506 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 554 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 558 | })(-1); | |
| 508 | 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 | 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 | 561 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 511 | 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(); | |
| 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()); | |
| 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(); | |
| 563 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 566 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 516 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 517 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 518 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 519 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 520 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 521 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 522 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 523 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 524 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 525 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 567 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 568 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 569 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 570 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 571 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 572 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 573 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 574 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 575 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 576 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 526 | 577 | k-next from:body -> { | |
| 527 | 578 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 528 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>commits · long · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,33 +39,52 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <div class="filter"> | |
| 63 | - | <label for="commit-filter">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)); | |
| 86 | + | <label for="commit-filter" class="sr">filter</label> | |
| 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 | 88 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | |
| 66 | 89 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | |
| 67 | 90 | </div> | |
| @@ -101,15 +124,13 @@ | |||
| 101 | 124 | </section> | |
| 102 | 125 | </div> | |
| 103 | 126 | <nav class="pager" aria-label="Pages"> | |
| 104 | - | <a href="/long/commits/main">← newest</a> | |
| 105 | - | <span class="mute">older →</span> | |
| 127 | + | <a class="btn" href="/long/commits/main">← Newest</a> | |
| 128 | + | <span class="btn" aria-disabled="true">Older →</span> | |
| 106 | 129 | </nav> | |
| 107 | 130 | </main> | |
| 108 | 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 | 132 | <section aria-labelledby="r-hist"> | |
| 112 | - | <h2 id="r-hist">history</h2> | |
| 133 | + | <h2 id="r-hist">History</h2> | |
| 113 | 134 | <dl class="kv"> | |
| 114 | 135 | <dt>ref</dt> | |
| 115 | 136 | <dd>main</dd> | |
| @@ -118,76 +139,76 @@ | |||
| 118 | 139 | </dl> | |
| 119 | 140 | </section> | |
| 120 | 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 | 143 | <p class="dim">no tag or branch head here</p> | |
| 123 | 144 | </section> | |
| 124 | - | </details> | |
| 125 | 145 | </aside> | |
| 126 | 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 | 162 | <div class="panel-head"> | |
| 129 | - | <span>keys</span> | |
| 130 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 163 | + | <h2>Keyboard shortcuts</h2> | |
| 164 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 165 | + | <kbd>esc</kbd> close</button> | |
| 131 | 166 | </div> | |
| 132 | 167 | <dl class="keys"> | |
| 133 | 168 | <div> | |
| 134 | 169 | <dt> | |
| 135 | - | <kbd>j</kbd> | |
| 136 | - | <kbd>k</kbd> | |
| 170 | + | <kbd>ctrl k</kbd> | |
| 171 | + | <kbd>:</kbd> | |
| 137 | 172 | </dt> | |
| 138 | - | <dd>move cursor down / up</dd> | |
| 173 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 139 | 174 | </div> | |
| 140 | 175 | <div> | |
| 141 | 176 | <dt> | |
| 142 | - | <kbd>enter</kbd> | |
| 177 | + | <kbd>?</kbd> | |
| 143 | 178 | </dt> | |
| 144 | - | <dd>open row under cursor</dd> | |
| 179 | + | <dd>this list</dd> | |
| 145 | 180 | </div> | |
| 146 | 181 | <div> | |
| 147 | 182 | <dt> | |
| 148 | - | <kbd>g</kbd> | |
| 149 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 183 | + | <kbd>j</kbd> | |
| 184 | + | <kbd>k</kbd> | |
| 150 | 185 | </dt> | |
| 151 | - | <dd>first / last row</dd> | |
| 186 | + | <dd>move the row cursor down / up</dd> | |
| 152 | 187 | </div> | |
| 153 | 188 | <div> | |
| 154 | 189 | <dt> | |
| 155 | - | <kbd>/</kbd> | |
| 190 | + | <kbd>enter</kbd> | |
| 156 | 191 | </dt> | |
| 157 | - | <dd>focus the filter</dd> | |
| 192 | + | <dd>open the row under the cursor</dd> | |
| 158 | 193 | </div> | |
| 159 | 194 | <div> | |
| 160 | 195 | <dt> | |
| 161 | - | <kbd>g h</kbd> | |
| 162 | - | <kbd>g t</kbd> | |
| 196 | + | <kbd>g g</kbd> | |
| 197 | + | <kbd>G</kbd> | |
| 163 | 198 | </dt> | |
| 164 | - | <dd>go home / tree</dd> | |
| 199 | + | <dd>first / last row</dd> | |
| 165 | 200 | </div> | |
| 166 | 201 | <div> | |
| 167 | 202 | <dt> | |
| 168 | - | <kbd>g c</kbd> | |
| 169 | - | <kbd>g r</kbd> | |
| 203 | + | <kbd>/</kbd> | |
| 170 | 204 | </dt> | |
| 171 | - | <dd>go commits / refs</dd> | |
| 205 | + | <dd>filter the list</dd> | |
| 172 | 206 | </div> | |
| 173 | 207 | <div> | |
| 174 | 208 | <dt> | |
| 175 | 209 | <kbd>b</kbd> | |
| 176 | 210 | </dt> | |
| 177 | - | <dd>switch ref (branches, tags)</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> | |
| 211 | + | <dd>switch branch or tag</dd> | |
| 191 | 212 | </div> | |
| 192 | 213 | <div> | |
| 193 | 214 | <dt> | |
| @@ -195,15 +216,21 @@ | |||
| 195 | 216 | <kbd>t</kbd> | |
| 196 | 217 | <kbd>c</kbd> | |
| 197 | 218 | <kbd>r</kbd> | |
| 219 | + | <kbd>m</kbd> | |
| 198 | 220 | </dt> | |
| 199 | - | <dd>repo sections (when shown)</dd> | |
| 221 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 200 | 222 | </div> | |
| 201 | 223 | <div> | |
| 202 | 224 | <dt> | |
| 203 | - | <kbd>m</kbd> | |
| 204 | - | <kbd>g m</kbd> | |
| 225 | + | <kbd>g h</kbd> | |
| 205 | 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 | 234 | </div> | |
| 208 | 235 | <div> | |
| 209 | 236 | <dt> | |
| @@ -223,19 +250,21 @@ | |||
| 223 | 250 | <dt> | |
| 224 | 251 | <kbd>esc</kbd> | |
| 225 | 252 | </dt> | |
| 226 | - | <dd>close panel</dd> | |
| 253 | + | <dd>close a dialog, leave a field</dd> | |
| 227 | 254 | </div> | |
| 228 | 255 | </dl> | |
| 229 | 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 | 258 | <div class="panel-head"> | |
| 232 | - | <label for="refs-filter">ref></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))"> | |
| 234 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 259 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 261 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 262 | + | <kbd>esc</kbd> | |
| 263 | + | </button> | |
| 235 | 264 | </div> | |
| 236 | 265 | <div class="panel-cols" data-picklist> | |
| 237 | 266 | <section aria-label="Branches"> | |
| 238 | - | <h2>branches <span class="mute">1</span> | |
| 267 | + | <h2>Branches <span class="count">1</span> | |
| 239 | 268 | </h2> | |
| 240 | 269 | <ul> | |
| 241 | 270 | <li> | |
| @@ -245,109 +274,125 @@ | |||
| 245 | 274 | </ul> | |
| 246 | 275 | </section> | |
| 247 | 276 | <section aria-label="Tags"> | |
| 248 | - | <h2>tags <span class="mute">0</span> | |
| 277 | + | <h2>Tags <span class="count">0</span> | |
| 249 | 278 | </h2> | |
| 250 | 279 | <ul> | |
| 251 | 280 | </ul> | |
| 252 | 281 | </section> | |
| 253 | 282 | </div> | |
| 254 | 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 | 285 | <div class="panel-head"> | |
| 257 | - | <label for="cmd-q" class="hi">:</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(); | |
| 286 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 288 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 260 | 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 | 290 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 262 | 291 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 263 | 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 | 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 | 297 | </div> | |
| 267 | - | <ul id="cmd-list" class="cmd-list"> | |
| 298 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 268 | 299 | <li data-k="go home" data-cursor> | |
| 269 | 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 | 304 | </li> | |
| 272 | 305 | <li data-k="section summary"> | |
| 273 | 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 | 310 | </li> | |
| 276 | - | <li data-k="section tree"> | |
| 311 | + | <li data-k="section files"> | |
| 277 | 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 | 316 | </li> | |
| 280 | 317 | <li data-k="section commits"> | |
| 281 | 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 | 322 | </li> | |
| 284 | - | <li data-k="section refs"> | |
| 323 | + | <li data-k="section branches & tags"> | |
| 285 | 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 | 328 | </li> | |
| 288 | 329 | <li data-k="section merge requests"> | |
| 289 | 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 | 334 | </li> | |
| 292 | 335 | <li data-k="branch main"> | |
| 293 | 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 | 340 | </li> | |
| 296 | 341 | <li data-k="repo diffs"> | |
| 297 | 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 | 346 | </li> | |
| 300 | 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 | 357 | </dialog> | |
| 302 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 358 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 318 | 359 | if (q('dialog[open]').count) return; | |
| 319 | 360 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 320 | 361 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 321 | 362 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 322 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 323 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 363 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 367 | })(1); | |
| 325 | - | k-k from:body -> ((d) => { | |
| 368 | + | k-k from:body -> (async (d) => { | |
| 326 | 369 | if (q('dialog[open]').count) return; | |
| 327 | 370 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 328 | 371 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 329 | 372 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 330 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 331 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 373 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 377 | })(-1); | |
| 333 | 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 | 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 | 380 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 336 | 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(); | |
| 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()); | |
| 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(); | |
| 382 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 385 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 341 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 342 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 343 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 344 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 345 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 346 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 347 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 348 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 349 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 350 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 386 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 387 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 388 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 389 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 390 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 391 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 392 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 393 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 394 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 395 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 351 | 396 | k-next from:body -> { | |
| 352 | 397 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 353 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>history of a.txt · long · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,33 +42,52 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <div class="filter"> | |
| 66 | - | <label for="commit-filter">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)); | |
| 89 | + | <label for="commit-filter" class="sr">filter</label> | |
| 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 | 91 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | |
| 69 | 92 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | |
| 70 | 93 | </div> | |
| @@ -115,10 +138,8 @@ | |||
| 115 | 138 | </div> | |
| 116 | 139 | </main> | |
| 117 | 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 | 141 | <section aria-labelledby="r-hist"> | |
| 121 | - | <h2 id="r-hist">history</h2> | |
| 142 | + | <h2 id="r-hist">History</h2> | |
| 122 | 143 | <dl class="kv"> | |
| 123 | 144 | <dt>ref</dt> | |
| 124 | 145 | <dd>main</dd> | |
| @@ -133,76 +154,76 @@ | |||
| 133 | 154 | </dl> | |
| 134 | 155 | </section> | |
| 135 | 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 | 158 | <p class="dim">no tag or branch head here</p> | |
| 138 | 159 | </section> | |
| 139 | - | </details> | |
| 140 | 160 | </aside> | |
| 141 | 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 | 177 | <div class="panel-head"> | |
| 144 | - | <span>keys</span> | |
| 145 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 178 | + | <h2>Keyboard shortcuts</h2> | |
| 179 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 180 | + | <kbd>esc</kbd> close</button> | |
| 146 | 181 | </div> | |
| 147 | 182 | <dl class="keys"> | |
| 148 | 183 | <div> | |
| 149 | 184 | <dt> | |
| 150 | - | <kbd>j</kbd> | |
| 151 | - | <kbd>k</kbd> | |
| 185 | + | <kbd>ctrl k</kbd> | |
| 186 | + | <kbd>:</kbd> | |
| 152 | 187 | </dt> | |
| 153 | - | <dd>move cursor down / up</dd> | |
| 188 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 154 | 189 | </div> | |
| 155 | 190 | <div> | |
| 156 | 191 | <dt> | |
| 157 | - | <kbd>enter</kbd> | |
| 192 | + | <kbd>?</kbd> | |
| 158 | 193 | </dt> | |
| 159 | - | <dd>open row under cursor</dd> | |
| 194 | + | <dd>this list</dd> | |
| 160 | 195 | </div> | |
| 161 | 196 | <div> | |
| 162 | 197 | <dt> | |
| 163 | - | <kbd>g</kbd> | |
| 164 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 198 | + | <kbd>j</kbd> | |
| 199 | + | <kbd>k</kbd> | |
| 165 | 200 | </dt> | |
| 166 | - | <dd>first / last row</dd> | |
| 201 | + | <dd>move the row cursor down / up</dd> | |
| 167 | 202 | </div> | |
| 168 | 203 | <div> | |
| 169 | 204 | <dt> | |
| 170 | - | <kbd>/</kbd> | |
| 205 | + | <kbd>enter</kbd> | |
| 171 | 206 | </dt> | |
| 172 | - | <dd>focus the filter</dd> | |
| 207 | + | <dd>open the row under the cursor</dd> | |
| 173 | 208 | </div> | |
| 174 | 209 | <div> | |
| 175 | 210 | <dt> | |
| 176 | - | <kbd>g h</kbd> | |
| 177 | - | <kbd>g t</kbd> | |
| 211 | + | <kbd>g g</kbd> | |
| 212 | + | <kbd>G</kbd> | |
| 178 | 213 | </dt> | |
| 179 | - | <dd>go home / tree</dd> | |
| 214 | + | <dd>first / last row</dd> | |
| 180 | 215 | </div> | |
| 181 | 216 | <div> | |
| 182 | 217 | <dt> | |
| 183 | - | <kbd>g c</kbd> | |
| 184 | - | <kbd>g r</kbd> | |
| 218 | + | <kbd>/</kbd> | |
| 185 | 219 | </dt> | |
| 186 | - | <dd>go commits / refs</dd> | |
| 220 | + | <dd>filter the list</dd> | |
| 187 | 221 | </div> | |
| 188 | 222 | <div> | |
| 189 | 223 | <dt> | |
| 190 | 224 | <kbd>b</kbd> | |
| 191 | 225 | </dt> | |
| 192 | - | <dd>switch ref (branches, tags)</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> | |
| 226 | + | <dd>switch branch or tag</dd> | |
| 206 | 227 | </div> | |
| 207 | 228 | <div> | |
| 208 | 229 | <dt> | |
| @@ -210,15 +231,21 @@ | |||
| 210 | 231 | <kbd>t</kbd> | |
| 211 | 232 | <kbd>c</kbd> | |
| 212 | 233 | <kbd>r</kbd> | |
| 234 | + | <kbd>m</kbd> | |
| 213 | 235 | </dt> | |
| 214 | - | <dd>repo sections (when shown)</dd> | |
| 236 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 215 | 237 | </div> | |
| 216 | 238 | <div> | |
| 217 | 239 | <dt> | |
| 218 | - | <kbd>m</kbd> | |
| 219 | - | <kbd>g m</kbd> | |
| 240 | + | <kbd>g h</kbd> | |
| 220 | 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 | 249 | </div> | |
| 223 | 250 | <div> | |
| 224 | 251 | <dt> | |
| @@ -238,19 +265,21 @@ | |||
| 238 | 265 | <dt> | |
| 239 | 266 | <kbd>esc</kbd> | |
| 240 | 267 | </dt> | |
| 241 | - | <dd>close panel</dd> | |
| 268 | + | <dd>close a dialog, leave a field</dd> | |
| 242 | 269 | </div> | |
| 243 | 270 | </dl> | |
| 244 | 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 | 273 | <div class="panel-head"> | |
| 247 | - | <label for="refs-filter">ref></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))"> | |
| 249 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 274 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 276 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 277 | + | <kbd>esc</kbd> | |
| 278 | + | </button> | |
| 250 | 279 | </div> | |
| 251 | 280 | <div class="panel-cols" data-picklist> | |
| 252 | 281 | <section aria-label="Branches"> | |
| 253 | - | <h2>branches <span class="mute">1</span> | |
| 282 | + | <h2>Branches <span class="count">1</span> | |
| 254 | 283 | </h2> | |
| 255 | 284 | <ul> | |
| 256 | 285 | <li> | |
| @@ -260,109 +289,125 @@ | |||
| 260 | 289 | </ul> | |
| 261 | 290 | </section> | |
| 262 | 291 | <section aria-label="Tags"> | |
| 263 | - | <h2>tags <span class="mute">0</span> | |
| 292 | + | <h2>Tags <span class="count">0</span> | |
| 264 | 293 | </h2> | |
| 265 | 294 | <ul> | |
| 266 | 295 | </ul> | |
| 267 | 296 | </section> | |
| 268 | 297 | </div> | |
| 269 | 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 | 300 | <div class="panel-head"> | |
| 272 | - | <label for="cmd-q" class="hi">:</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(); | |
| 301 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 303 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 275 | 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 | 305 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 277 | 306 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 278 | 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 | 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 | 312 | </div> | |
| 282 | - | <ul id="cmd-list" class="cmd-list"> | |
| 313 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 283 | 314 | <li data-k="go home" data-cursor> | |
| 284 | 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 | 319 | </li> | |
| 287 | 320 | <li data-k="section summary"> | |
| 288 | 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 | 325 | </li> | |
| 291 | - | <li data-k="section tree"> | |
| 326 | + | <li data-k="section files"> | |
| 292 | 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 | 331 | </li> | |
| 295 | 332 | <li data-k="section commits"> | |
| 296 | 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 | 337 | </li> | |
| 299 | - | <li data-k="section refs"> | |
| 338 | + | <li data-k="section branches & tags"> | |
| 300 | 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 | 343 | </li> | |
| 303 | 344 | <li data-k="section merge requests"> | |
| 304 | 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 | 349 | </li> | |
| 307 | 350 | <li data-k="branch main"> | |
| 308 | 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 | 355 | </li> | |
| 311 | 356 | <li data-k="repo diffs"> | |
| 312 | 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 | 361 | </li> | |
| 315 | 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 | 372 | </dialog> | |
| 317 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 373 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 333 | 374 | if (q('dialog[open]').count) return; | |
| 334 | 375 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 335 | 376 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 336 | 377 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 337 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 338 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 378 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 382 | })(1); | |
| 340 | - | k-k from:body -> ((d) => { | |
| 383 | + | k-k from:body -> (async (d) => { | |
| 341 | 384 | if (q('dialog[open]').count) return; | |
| 342 | 385 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 343 | 386 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 344 | 387 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 345 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 346 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 388 | + | 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' }); | |
| 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 | 392 | })(-1); | |
| 348 | 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 | 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 | 395 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 351 | 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(); | |
| 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()); | |
| 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(); | |
| 397 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 400 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 356 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 357 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 358 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 359 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 360 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 361 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 362 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 363 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 364 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 365 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 401 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 402 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 403 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 404 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 405 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 406 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 407 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 408 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 409 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 410 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 366 | 411 | k-next from:body -> { | |
| 367 | 412 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 368 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>commits · long · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,33 +39,52 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <div class="filter"> | |
| 63 | - | <label for="commit-filter">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)); | |
| 86 | + | <label for="commit-filter" class="sr">filter</label> | |
| 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 | 88 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | |
| 66 | 89 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | |
| 67 | 90 | </div> | |
| @@ -80,7 +103,7 @@ | |||
| 80 | 103 | <td class="c-sha mute">SHA7</td> | |
| 81 | 104 | <td class="c-subject"> | |
| 82 | 105 | <a href="/long/commit/SHA40">commit number 51</a> | |
| 83 | - | <span class="tagged">[main]</span> | |
| 106 | + | <span class="pill">main</span> | |
| 84 | 107 | </td> | |
| 85 | 108 | <td class="c-who dim">T</td> | |
| 86 | 109 | <td class="c-when nowrap dim"> | |
| @@ -896,15 +919,13 @@ | |||
| 896 | 919 | </section> | |
| 897 | 920 | </div> | |
| 898 | 921 | <nav class="pager" aria-label="Pages"> | |
| 899 | - | <span class="mute">← newest</span> | |
| 900 | - | <a href="/long/commits/main?page=YzE6MzMzZGFjMWY5YmY4MjIzMmQxOTAzNGVlZTliZmI2NzFmM2QxMWEzNDo1MDplM2IwYzQ0Mjk4ZmM" rel="next">older →</a> | |
| 922 | + | <span class="btn" aria-disabled="true">← Newest</span> | |
| 923 | + | <a class="btn" href="/long/commits/main?page=YzE6MzMzZGFjMWY5YmY4MjIzMmQxOTAzNGVlZTliZmI2NzFmM2QxMWEzNDo1MDplM2IwYzQ0Mjk4ZmM" rel="next">Older →</a> | |
| 901 | 924 | </nav> | |
| 902 | 925 | </main> | |
| 903 | 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 | 927 | <section aria-labelledby="r-hist"> | |
| 907 | - | <h2 id="r-hist">history</h2> | |
| 928 | + | <h2 id="r-hist">History</h2> | |
| 908 | 929 | <dl class="kv"> | |
| 909 | 930 | <dt>ref</dt> | |
| 910 | 931 | <dd>main</dd> | |
| @@ -913,78 +934,83 @@ | |||
| 913 | 934 | </dl> | |
| 914 | 935 | </section> | |
| 915 | 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 | 938 | <nav class="outline" aria-label="Refs on this page"> | |
| 918 | 939 | <a href="#c-SHA7">branch main</a> | |
| 919 | 940 | </nav> | |
| 920 | 941 | </section> | |
| 921 | - | </details> | |
| 922 | 942 | </aside> | |
| 923 | 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 | 964 | <div class="panel-head"> | |
| 926 | - | <span>keys</span> | |
| 927 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 965 | + | <h2>Keyboard shortcuts</h2> | |
| 966 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 967 | + | <kbd>esc</kbd> close</button> | |
| 928 | 968 | </div> | |
| 929 | 969 | <dl class="keys"> | |
| 930 | 970 | <div> | |
| 931 | 971 | <dt> | |
| 932 | - | <kbd>j</kbd> | |
| 933 | - | <kbd>k</kbd> | |
| 972 | + | <kbd>ctrl k</kbd> | |
| 973 | + | <kbd>:</kbd> | |
| 934 | 974 | </dt> | |
| 935 | - | <dd>move cursor down / up</dd> | |
| 975 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 936 | 976 | </div> | |
| 937 | 977 | <div> | |
| 938 | 978 | <dt> | |
| 939 | - | <kbd>enter</kbd> | |
| 979 | + | <kbd>?</kbd> | |
| 940 | 980 | </dt> | |
| 941 | - | <dd>open row under cursor</dd> | |
| 981 | + | <dd>this list</dd> | |
| 942 | 982 | </div> | |
| 943 | 983 | <div> | |
| 944 | 984 | <dt> | |
| 945 | - | <kbd>g</kbd> | |
| 946 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 985 | + | <kbd>j</kbd> | |
| 986 | + | <kbd>k</kbd> | |
| 947 | 987 | </dt> | |
| 948 | - | <dd>first / last row</dd> | |
| 988 | + | <dd>move the row cursor down / up</dd> | |
| 949 | 989 | </div> | |
| 950 | 990 | <div> | |
| 951 | 991 | <dt> | |
| 952 | - | <kbd>/</kbd> | |
| 992 | + | <kbd>enter</kbd> | |
| 953 | 993 | </dt> | |
| 954 | - | <dd>focus the filter</dd> | |
| 994 | + | <dd>open the row under the cursor</dd> | |
| 955 | 995 | </div> | |
| 956 | 996 | <div> | |
| 957 | 997 | <dt> | |
| 958 | - | <kbd>g h</kbd> | |
| 959 | - | <kbd>g t</kbd> | |
| 998 | + | <kbd>g g</kbd> | |
| 999 | + | <kbd>G</kbd> | |
| 960 | 1000 | </dt> | |
| 961 | - | <dd>go home / tree</dd> | |
| 1001 | + | <dd>first / last row</dd> | |
| 962 | 1002 | </div> | |
| 963 | 1003 | <div> | |
| 964 | 1004 | <dt> | |
| 965 | - | <kbd>g c</kbd> | |
| 966 | - | <kbd>g r</kbd> | |
| 1005 | + | <kbd>/</kbd> | |
| 967 | 1006 | </dt> | |
| 968 | - | <dd>go commits / refs</dd> | |
| 1007 | + | <dd>filter the list</dd> | |
| 969 | 1008 | </div> | |
| 970 | 1009 | <div> | |
| 971 | 1010 | <dt> | |
| 972 | 1011 | <kbd>b</kbd> | |
| 973 | 1012 | </dt> | |
| 974 | - | <dd>switch ref (branches, tags)</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> | |
| 1013 | + | <dd>switch branch or tag</dd> | |
| 988 | 1014 | </div> | |
| 989 | 1015 | <div> | |
| 990 | 1016 | <dt> | |
| @@ -992,15 +1018,21 @@ | |||
| 992 | 1018 | <kbd>t</kbd> | |
| 993 | 1019 | <kbd>c</kbd> | |
| 994 | 1020 | <kbd>r</kbd> | |
| 1021 | + | <kbd>m</kbd> | |
| 995 | 1022 | </dt> | |
| 996 | - | <dd>repo sections (when shown)</dd> | |
| 1023 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 997 | 1024 | </div> | |
| 998 | 1025 | <div> | |
| 999 | 1026 | <dt> | |
| 1000 | - | <kbd>m</kbd> | |
| 1001 | - | <kbd>g m</kbd> | |
| 1027 | + | <kbd>g h</kbd> | |
| 1028 | + | </dt> | |
| 1029 | + | <dd>home</dd> | |
| 1030 | + | </div> | |
| 1031 | + | <div> | |
| 1032 | + | <dt> | |
| 1033 | + | <kbd>y</kbd> | |
| 1002 | 1034 | </dt> | |
| 1003 | - | <dd>merge requests (when shown)</dd> | |
| 1035 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 1004 | 1036 | </div> | |
| 1005 | 1037 | <div> | |
| 1006 | 1038 | <dt> | |
| @@ -1020,19 +1052,21 @@ | |||
| 1020 | 1052 | <dt> | |
| 1021 | 1053 | <kbd>esc</kbd> | |
| 1022 | 1054 | </dt> | |
| 1023 | - | <dd>close panel</dd> | |
| 1055 | + | <dd>close a dialog, leave a field</dd> | |
| 1024 | 1056 | </div> | |
| 1025 | 1057 | </dl> | |
| 1026 | 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 | 1060 | <div class="panel-head"> | |
| 1029 | - | <label for="refs-filter">ref></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))"> | |
| 1031 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 1061 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 1063 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 1064 | + | <kbd>esc</kbd> | |
| 1065 | + | </button> | |
| 1032 | 1066 | </div> | |
| 1033 | 1067 | <div class="panel-cols" data-picklist> | |
| 1034 | 1068 | <section aria-label="Branches"> | |
| 1035 | - | <h2>branches <span class="mute">1</span> | |
| 1069 | + | <h2>Branches <span class="count">1</span> | |
| 1036 | 1070 | </h2> | |
| 1037 | 1071 | <ul> | |
| 1038 | 1072 | <li> | |
| @@ -1042,112 +1076,125 @@ | |||
| 1042 | 1076 | </ul> | |
| 1043 | 1077 | </section> | |
| 1044 | 1078 | <section aria-label="Tags"> | |
| 1045 | - | <h2>tags <span class="mute">0</span> | |
| 1079 | + | <h2>Tags <span class="count">0</span> | |
| 1046 | 1080 | </h2> | |
| 1047 | 1081 | <ul> | |
| 1048 | 1082 | </ul> | |
| 1049 | 1083 | </section> | |
| 1050 | 1084 | </div> | |
| 1051 | 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 | 1087 | <div class="panel-head"> | |
| 1054 | - | <label for="cmd-q" class="hi">:</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(); | |
| 1088 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 1090 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 1057 | 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 | 1092 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 1059 | 1093 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 1060 | 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 | 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 | 1099 | </div> | |
| 1064 | - | <ul id="cmd-list" class="cmd-list"> | |
| 1100 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 1065 | 1101 | <li data-k="go home" data-cursor> | |
| 1066 | 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 | 1106 | </li> | |
| 1069 | 1107 | <li data-k="section summary"> | |
| 1070 | 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 | 1112 | </li> | |
| 1073 | - | <li data-k="section tree"> | |
| 1113 | + | <li data-k="section files"> | |
| 1074 | 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 | 1118 | </li> | |
| 1077 | 1119 | <li data-k="section commits"> | |
| 1078 | 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 | 1124 | </li> | |
| 1081 | - | <li data-k="section refs"> | |
| 1125 | + | <li data-k="section branches & tags"> | |
| 1082 | 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 | 1130 | </li> | |
| 1085 | 1131 | <li data-k="section merge requests"> | |
| 1086 | 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 | 1136 | </li> | |
| 1089 | 1137 | <li data-k="branch main"> | |
| 1090 | 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 | 1142 | </li> | |
| 1093 | 1143 | <li data-k="repo diffs"> | |
| 1094 | 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 | 1148 | </li> | |
| 1097 | 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 | 1159 | </dialog> | |
| 1099 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 1160 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 1118 | 1161 | if (q('dialog[open]').count) return; | |
| 1119 | 1162 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 1120 | 1163 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 1121 | 1164 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 1122 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 1123 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 1165 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 1169 | })(1); | |
| 1125 | - | k-k from:body -> ((d) => { | |
| 1170 | + | k-k from:body -> (async (d) => { | |
| 1126 | 1171 | if (q('dialog[open]').count) return; | |
| 1127 | 1172 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 1128 | 1173 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 1129 | 1174 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 1130 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 1131 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 1175 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 1179 | })(-1); | |
| 1133 | 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 | 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 | 1182 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 1136 | 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(); | |
| 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()); | |
| 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(); | |
| 1184 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 1187 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 1141 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 1142 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 1143 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 1144 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 1145 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 1146 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 1147 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 1148 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 1149 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 1150 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 1188 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 1189 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 1190 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 1191 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 1192 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 1193 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 1194 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 1195 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 1196 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 1197 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 1151 | 1198 | k-next from:body -> { | |
| 1152 | 1199 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 1153 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>commits · diffs · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,33 +39,52 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <div class="filter"> | |
| 63 | - | <label for="commit-filter">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)); | |
| 86 | + | <label for="commit-filter" class="sr">filter</label> | |
| 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 | 88 | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | |
| 66 | 89 | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | |
| 67 | 90 | </div> | |
| @@ -80,7 +103,7 @@ | |||
| 80 | 103 | <td class="c-sha mute">SHA7</td> | |
| 81 | 104 | <td class="c-subject"> | |
| 82 | 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 | 107 | </td> | |
| 85 | 108 | <td class="c-who dim">T</td> | |
| 86 | 109 | <td class="c-when nowrap dim"> | |
| @@ -123,7 +146,7 @@ | |||
| 123 | 146 | <td class="c-sha mute">SHA7</td> | |
| 124 | 147 | <td class="c-subject"> | |
| 125 | 148 | <a href="/diffs/commit/SHA40">merge the side branch</a> | |
| 126 | - | <span class="tagged">(merge)</span> | |
| 149 | + | <span class="pill">merge</span> | |
| 127 | 150 | </td> | |
| 128 | 151 | <td class="c-who dim">T</td> | |
| 129 | 152 | <td class="c-when nowrap dim"> | |
| @@ -145,7 +168,7 @@ | |||
| 145 | 168 | <td class="c-sha mute">SHA7</td> | |
| 146 | 169 | <td class="c-subject"> | |
| 147 | 170 | <a href="/diffs/commit/SHA40">work on the side</a> | |
| 148 | - | <span class="tagged">[side]</span> | |
| 171 | + | <span class="pill">side</span> | |
| 149 | 172 | </td> | |
| 150 | 173 | <td class="c-who dim">T</td> | |
| 151 | 174 | <td class="c-when nowrap dim"> | |
| @@ -209,7 +232,8 @@ | |||
| 209 | 232 | <td class="c-sha mute">SHA7</td> | |
| 210 | 233 | <td class="c-subject"> | |
| 211 | 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 | 237 | </td> | |
| 214 | 238 | <td class="c-who dim">T</td> | |
| 215 | 239 | <td class="c-when nowrap dim"> | |
| @@ -243,10 +267,8 @@ | |||
| 243 | 267 | </div> | |
| 244 | 268 | </main> | |
| 245 | 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 | 270 | <section aria-labelledby="r-hist"> | |
| 249 | - | <h2 id="r-hist">history</h2> | |
| 271 | + | <h2 id="r-hist">History</h2> | |
| 250 | 272 | <dl class="kv"> | |
| 251 | 273 | <dt>ref</dt> | |
| 252 | 274 | <dd>main</dd> | |
| @@ -255,80 +277,85 @@ | |||
| 255 | 277 | </dl> | |
| 256 | 278 | </section> | |
| 257 | 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 | 281 | <nav class="outline" aria-label="Refs on this page"> | |
| 260 | 282 | <a href="#c-SHA7">branch main</a> | |
| 261 | 283 | <a href="#c-SHA7">branch side</a> | |
| 262 | 284 | <a href="#c-SHA7">tag v1.0.0</a> | |
| 263 | 285 | </nav> | |
| 264 | 286 | </section> | |
| 265 | - | </details> | |
| 266 | 287 | </aside> | |
| 267 | 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 | 309 | <div class="panel-head"> | |
| 270 | - | <span>keys</span> | |
| 271 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 310 | + | <h2>Keyboard shortcuts</h2> | |
| 311 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 312 | + | <kbd>esc</kbd> close</button> | |
| 272 | 313 | </div> | |
| 273 | 314 | <dl class="keys"> | |
| 274 | 315 | <div> | |
| 275 | 316 | <dt> | |
| 276 | - | <kbd>j</kbd> | |
| 277 | - | <kbd>k</kbd> | |
| 317 | + | <kbd>ctrl k</kbd> | |
| 318 | + | <kbd>:</kbd> | |
| 278 | 319 | </dt> | |
| 279 | - | <dd>move cursor down / up</dd> | |
| 320 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 280 | 321 | </div> | |
| 281 | 322 | <div> | |
| 282 | 323 | <dt> | |
| 283 | - | <kbd>enter</kbd> | |
| 324 | + | <kbd>?</kbd> | |
| 284 | 325 | </dt> | |
| 285 | - | <dd>open row under cursor</dd> | |
| 326 | + | <dd>this list</dd> | |
| 286 | 327 | </div> | |
| 287 | 328 | <div> | |
| 288 | 329 | <dt> | |
| 289 | - | <kbd>g</kbd> | |
| 290 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 330 | + | <kbd>j</kbd> | |
| 331 | + | <kbd>k</kbd> | |
| 291 | 332 | </dt> | |
| 292 | - | <dd>first / last row</dd> | |
| 333 | + | <dd>move the row cursor down / up</dd> | |
| 293 | 334 | </div> | |
| 294 | 335 | <div> | |
| 295 | 336 | <dt> | |
| 296 | - | <kbd>/</kbd> | |
| 337 | + | <kbd>enter</kbd> | |
| 297 | 338 | </dt> | |
| 298 | - | <dd>focus the filter</dd> | |
| 339 | + | <dd>open the row under the cursor</dd> | |
| 299 | 340 | </div> | |
| 300 | 341 | <div> | |
| 301 | 342 | <dt> | |
| 302 | - | <kbd>g h</kbd> | |
| 303 | - | <kbd>g t</kbd> | |
| 343 | + | <kbd>g g</kbd> | |
| 344 | + | <kbd>G</kbd> | |
| 304 | 345 | </dt> | |
| 305 | - | <dd>go home / tree</dd> | |
| 346 | + | <dd>first / last row</dd> | |
| 306 | 347 | </div> | |
| 307 | 348 | <div> | |
| 308 | 349 | <dt> | |
| 309 | - | <kbd>g c</kbd> | |
| 310 | - | <kbd>g r</kbd> | |
| 350 | + | <kbd>/</kbd> | |
| 311 | 351 | </dt> | |
| 312 | - | <dd>go commits / refs</dd> | |
| 352 | + | <dd>filter the list</dd> | |
| 313 | 353 | </div> | |
| 314 | 354 | <div> | |
| 315 | 355 | <dt> | |
| 316 | 356 | <kbd>b</kbd> | |
| 317 | 357 | </dt> | |
| 318 | - | <dd>switch ref (branches, tags)</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> | |
| 358 | + | <dd>switch branch or tag</dd> | |
| 332 | 359 | </div> | |
| 333 | 360 | <div> | |
| 334 | 361 | <dt> | |
| @@ -336,15 +363,21 @@ | |||
| 336 | 363 | <kbd>t</kbd> | |
| 337 | 364 | <kbd>c</kbd> | |
| 338 | 365 | <kbd>r</kbd> | |
| 366 | + | <kbd>m</kbd> | |
| 339 | 367 | </dt> | |
| 340 | - | <dd>repo sections (when shown)</dd> | |
| 368 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 341 | 369 | </div> | |
| 342 | 370 | <div> | |
| 343 | 371 | <dt> | |
| 344 | - | <kbd>m</kbd> | |
| 345 | - | <kbd>g m</kbd> | |
| 372 | + | <kbd>g h</kbd> | |
| 373 | + | </dt> | |
| 374 | + | <dd>home</dd> | |
| 375 | + | </div> | |
| 376 | + | <div> | |
| 377 | + | <dt> | |
| 378 | + | <kbd>y</kbd> | |
| 346 | 379 | </dt> | |
| 347 | - | <dd>merge requests (when shown)</dd> | |
| 380 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 348 | 381 | </div> | |
| 349 | 382 | <div> | |
| 350 | 383 | <dt> | |
| @@ -364,19 +397,21 @@ | |||
| 364 | 397 | <dt> | |
| 365 | 398 | <kbd>esc</kbd> | |
| 366 | 399 | </dt> | |
| 367 | - | <dd>close panel</dd> | |
| 400 | + | <dd>close a dialog, leave a field</dd> | |
| 368 | 401 | </div> | |
| 369 | 402 | </dl> | |
| 370 | 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 | 405 | <div class="panel-head"> | |
| 373 | - | <label for="refs-filter">ref></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))"> | |
| 375 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 406 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 408 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 409 | + | <kbd>esc</kbd> | |
| 410 | + | </button> | |
| 376 | 411 | </div> | |
| 377 | 412 | <div class="panel-cols" data-picklist> | |
| 378 | 413 | <section aria-label="Branches"> | |
| 379 | - | <h2>branches <span class="mute">2</span> | |
| 414 | + | <h2>Branches <span class="count">2</span> | |
| 380 | 415 | </h2> | |
| 381 | 416 | <ul> | |
| 382 | 417 | <li> | |
| @@ -390,7 +425,7 @@ | |||
| 390 | 425 | </ul> | |
| 391 | 426 | </section> | |
| 392 | 427 | <section aria-label="Tags"> | |
| 393 | - | <h2>tags <span class="mute">1</span> | |
| 428 | + | <h2>Tags <span class="count">1</span> | |
| 394 | 429 | </h2> | |
| 395 | 430 | <ul> | |
| 396 | 431 | <li> | |
| @@ -401,113 +436,130 @@ | |||
| 401 | 436 | </section> | |
| 402 | 437 | </div> | |
| 403 | 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 | 440 | <div class="panel-head"> | |
| 406 | - | <label for="cmd-q" class="hi">:</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(); | |
| 441 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 443 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 409 | 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 | 445 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 411 | 446 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 412 | 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 | 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 | 452 | </div> | |
| 416 | - | <ul id="cmd-list" class="cmd-list"> | |
| 453 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 417 | 454 | <li data-k="go home" data-cursor> | |
| 418 | 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 | 459 | </li> | |
| 421 | 460 | <li data-k="section summary"> | |
| 422 | 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 | 465 | </li> | |
| 425 | - | <li data-k="section tree"> | |
| 466 | + | <li data-k="section files"> | |
| 426 | 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 | 471 | </li> | |
| 429 | 472 | <li data-k="section commits"> | |
| 430 | 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 | 477 | </li> | |
| 433 | - | <li data-k="section refs"> | |
| 478 | + | <li data-k="section branches & tags"> | |
| 434 | 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 | 483 | </li> | |
| 437 | 484 | <li data-k="section merge requests"> | |
| 438 | 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 | 489 | </li> | |
| 441 | 490 | <li data-k="branch main"> | |
| 442 | 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 | 495 | </li> | |
| 445 | 496 | <li data-k="branch side"> | |
| 446 | 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 | 501 | </li> | |
| 449 | 502 | <li data-k="tag v1.0.0"> | |
| 450 | 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 | 507 | </li> | |
| 453 | 508 | <li data-k="repo long"> | |
| 454 | 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 | 513 | </li> | |
| 457 | 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 | 524 | </dialog> | |
| 459 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 525 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 478 | 526 | if (q('dialog[open]').count) return; | |
| 479 | 527 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 480 | 528 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 481 | 529 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 482 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 483 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 530 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 534 | })(1); | |
| 485 | - | k-k from:body -> ((d) => { | |
| 535 | + | k-k from:body -> (async (d) => { | |
| 486 | 536 | if (q('dialog[open]').count) return; | |
| 487 | 537 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 488 | 538 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 489 | 539 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 490 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 491 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 540 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 544 | })(-1); | |
| 493 | 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 | 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 | 547 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 496 | 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(); | |
| 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()); | |
| 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(); | |
| 549 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 552 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 501 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 502 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 503 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 504 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 505 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 506 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 507 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 508 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 509 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 510 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 553 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 554 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 555 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 556 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 557 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 558 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 559 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 560 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 561 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 562 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 511 | 563 | k-next from:body -> { | |
| 512 | 564 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 513 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>quiet · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -29,23 +33,33 @@ | |||
| 29 | 33 | </li> | |
| 30 | 34 | </ol> | |
| 31 | 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 | 46 | </header> | |
| 33 | 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 | 49 | <section class="state" aria-labelledby="state-title"> | |
| 36 | - | <h1 id="state-title"> | |
| 37 | - | <span class="code">empty</span> quiet is empty</h1> | |
| 50 | + | <p class="eyebrow">empty</p> | |
| 51 | + | <h1 id="state-title">quiet is empty</h1> | |
| 38 | 52 | <p class="dim">no commits yet. push a branch to create one.</p> | |
| 39 | 53 | <div class="clone" id="clone-quiet"> | |
| 40 | 54 | <div class="clone-line"> | |
| 41 | 55 | <span class="mute" aria-hidden="true">$ </span> | |
| 42 | 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 | 58 | </div> | |
| 45 | 59 | <div class="clone-line"> | |
| 46 | 60 | <span class="mute" aria-hidden="true">$ </span> | |
| 47 | 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 | 63 | </div> | |
| 50 | 64 | </div> | |
| 51 | 65 | <p class="state-back"> | |
| @@ -54,70 +68,71 @@ | |||
| 54 | 68 | </section> | |
| 55 | 69 | </main> | |
| 56 | 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 | 86 | <div class="panel-head"> | |
| 59 | - | <span>keys</span> | |
| 60 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 87 | + | <h2>Keyboard shortcuts</h2> | |
| 88 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 89 | + | <kbd>esc</kbd> close</button> | |
| 61 | 90 | </div> | |
| 62 | 91 | <dl class="keys"> | |
| 63 | 92 | <div> | |
| 64 | 93 | <dt> | |
| 65 | - | <kbd>j</kbd> | |
| 66 | - | <kbd>k</kbd> | |
| 94 | + | <kbd>ctrl k</kbd> | |
| 95 | + | <kbd>:</kbd> | |
| 67 | 96 | </dt> | |
| 68 | - | <dd>move cursor down / up</dd> | |
| 97 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 69 | 98 | </div> | |
| 70 | 99 | <div> | |
| 71 | 100 | <dt> | |
| 72 | - | <kbd>enter</kbd> | |
| 101 | + | <kbd>?</kbd> | |
| 73 | 102 | </dt> | |
| 74 | - | <dd>open row under cursor</dd> | |
| 103 | + | <dd>this list</dd> | |
| 75 | 104 | </div> | |
| 76 | 105 | <div> | |
| 77 | 106 | <dt> | |
| 78 | - | <kbd>g</kbd> | |
| 79 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 107 | + | <kbd>j</kbd> | |
| 108 | + | <kbd>k</kbd> | |
| 80 | 109 | </dt> | |
| 81 | - | <dd>first / last row</dd> | |
| 110 | + | <dd>move the row cursor down / up</dd> | |
| 82 | 111 | </div> | |
| 83 | 112 | <div> | |
| 84 | 113 | <dt> | |
| 85 | - | <kbd>/</kbd> | |
| 114 | + | <kbd>enter</kbd> | |
| 86 | 115 | </dt> | |
| 87 | - | <dd>focus the filter</dd> | |
| 116 | + | <dd>open the row under the cursor</dd> | |
| 88 | 117 | </div> | |
| 89 | 118 | <div> | |
| 90 | 119 | <dt> | |
| 91 | - | <kbd>g h</kbd> | |
| 92 | - | <kbd>g t</kbd> | |
| 120 | + | <kbd>g g</kbd> | |
| 121 | + | <kbd>G</kbd> | |
| 93 | 122 | </dt> | |
| 94 | - | <dd>go home / tree</dd> | |
| 123 | + | <dd>first / last row</dd> | |
| 95 | 124 | </div> | |
| 96 | 125 | <div> | |
| 97 | 126 | <dt> | |
| 98 | - | <kbd>g c</kbd> | |
| 99 | - | <kbd>g r</kbd> | |
| 127 | + | <kbd>/</kbd> | |
| 100 | 128 | </dt> | |
| 101 | - | <dd>go commits / refs</dd> | |
| 129 | + | <dd>filter the list</dd> | |
| 102 | 130 | </div> | |
| 103 | 131 | <div> | |
| 104 | 132 | <dt> | |
| 105 | 133 | <kbd>b</kbd> | |
| 106 | 134 | </dt> | |
| 107 | - | <dd>switch ref (branches, tags)</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> | |
| 135 | + | <dd>switch branch or tag</dd> | |
| 121 | 136 | </div> | |
| 122 | 137 | <div> | |
| 123 | 138 | <dt> | |
| @@ -125,15 +140,21 @@ | |||
| 125 | 140 | <kbd>t</kbd> | |
| 126 | 141 | <kbd>c</kbd> | |
| 127 | 142 | <kbd>r</kbd> | |
| 143 | + | <kbd>m</kbd> | |
| 128 | 144 | </dt> | |
| 129 | - | <dd>repo sections (when shown)</dd> | |
| 145 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 130 | 146 | </div> | |
| 131 | 147 | <div> | |
| 132 | 148 | <dt> | |
| 133 | - | <kbd>m</kbd> | |
| 134 | - | <kbd>g m</kbd> | |
| 149 | + | <kbd>g h</kbd> | |
| 135 | 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 | 158 | </div> | |
| 138 | 159 | <div> | |
| 139 | 160 | <dt> | |
| @@ -153,81 +174,86 @@ | |||
| 153 | 174 | <dt> | |
| 154 | 175 | <kbd>esc</kbd> | |
| 155 | 176 | </dt> | |
| 156 | - | <dd>close panel</dd> | |
| 177 | + | <dd>close a dialog, leave a field</dd> | |
| 157 | 178 | </div> | |
| 158 | 179 | </dl> | |
| 159 | 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 | 182 | <div class="panel-head"> | |
| 162 | - | <label for="cmd-q" class="hi">:</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(); | |
| 183 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 185 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 165 | 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 | 187 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 167 | 188 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 168 | 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 | 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 | 194 | </div> | |
| 172 | - | <ul id="cmd-list" class="cmd-list"> | |
| 195 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 173 | 196 | <li data-k="go home" data-cursor> | |
| 174 | 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 | 201 | </li> | |
| 177 | 202 | <li data-k="repo demo"> | |
| 178 | 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 | 207 | </li> | |
| 181 | 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 | 218 | </dialog> | |
| 183 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 219 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 198 | 220 | if (q('dialog[open]').count) return; | |
| 199 | 221 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 200 | 222 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 201 | 223 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 202 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 203 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 224 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 228 | })(1); | |
| 205 | - | k-k from:body -> ((d) => { | |
| 229 | + | k-k from:body -> (async (d) => { | |
| 206 | 230 | if (q('dialog[open]').count) return; | |
| 207 | 231 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 208 | 232 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 209 | 233 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 210 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 211 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 234 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 238 | })(-1); | |
| 213 | 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 | 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 | 241 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 216 | 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(); | |
| 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()); | |
| 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(); | |
| 243 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 246 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 221 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 222 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 223 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 224 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 225 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 226 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 227 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 228 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 229 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 230 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 247 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 248 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 249 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 250 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 251 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 252 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 253 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 254 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 255 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 256 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 231 | 257 | k-next from:body -> { | |
| 232 | 258 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 233 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,29 +17,31 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 25 | - | <nav class="crumbs" aria-label="Path"> | |
| 26 | - | <ol> | |
| 27 | - | </ol> | |
| 28 | - | </nav> | |
| 29 | - | <nav class="secs" aria-label="Sections"> | |
| 30 | - | <ul> | |
| 31 | - | <li> | |
| 32 | - | <a href="/" data-key="h" aria-keyshortcuts="g h" aria-current="page">home</a> | |
| 33 | - | </li> | |
| 34 | - | </ul> | |
| 35 | - | </nav> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 29 | + | <div class="hdr-acts"> | |
| 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'))"> | |
| 31 | + | <span class="ico i-search" aria-hidden="true"> | |
| 32 | + | </span> | |
| 33 | + | <span class="search-label">Go to…</span> | |
| 34 | + | <kbd>ctrl k</kbd> | |
| 35 | + | </button> | |
| 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> | |
| 37 | + | </div> | |
| 38 | + | </div> | |
| 36 | 39 | </header> | |
| 37 | 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 | 42 | <div class="filter"> | |
| 40 | - | <label for="home-filter">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])')"> | |
| 43 | + | <label for="home-filter" class="sr">filter</label> | |
| 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 | 45 | </div> | |
| 43 | 46 | <table class="list home" aria-label="Repositories"> | |
| 44 | 47 | <thead> | |
| @@ -90,70 +93,76 @@ | |||
| 90 | 93 | </table> | |
| 91 | 94 | </main> | |
| 92 | 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 | 116 | <div class="panel-head"> | |
| 95 | - | <span>keys</span> | |
| 96 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 117 | + | <h2>Keyboard shortcuts</h2> | |
| 118 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 119 | + | <kbd>esc</kbd> close</button> | |
| 97 | 120 | </div> | |
| 98 | 121 | <dl class="keys"> | |
| 99 | 122 | <div> | |
| 100 | 123 | <dt> | |
| 101 | - | <kbd>j</kbd> | |
| 102 | - | <kbd>k</kbd> | |
| 124 | + | <kbd>ctrl k</kbd> | |
| 125 | + | <kbd>:</kbd> | |
| 103 | 126 | </dt> | |
| 104 | - | <dd>move cursor down / up</dd> | |
| 127 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 105 | 128 | </div> | |
| 106 | 129 | <div> | |
| 107 | 130 | <dt> | |
| 108 | - | <kbd>enter</kbd> | |
| 131 | + | <kbd>?</kbd> | |
| 109 | 132 | </dt> | |
| 110 | - | <dd>open row under cursor</dd> | |
| 133 | + | <dd>this list</dd> | |
| 111 | 134 | </div> | |
| 112 | 135 | <div> | |
| 113 | 136 | <dt> | |
| 114 | - | <kbd>g</kbd> | |
| 115 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 137 | + | <kbd>j</kbd> | |
| 138 | + | <kbd>k</kbd> | |
| 116 | 139 | </dt> | |
| 117 | - | <dd>first / last row</dd> | |
| 140 | + | <dd>move the row cursor down / up</dd> | |
| 118 | 141 | </div> | |
| 119 | 142 | <div> | |
| 120 | 143 | <dt> | |
| 121 | - | <kbd>/</kbd> | |
| 144 | + | <kbd>enter</kbd> | |
| 122 | 145 | </dt> | |
| 123 | - | <dd>focus the filter</dd> | |
| 146 | + | <dd>open the row under the cursor</dd> | |
| 124 | 147 | </div> | |
| 125 | 148 | <div> | |
| 126 | 149 | <dt> | |
| 127 | - | <kbd>g h</kbd> | |
| 128 | - | <kbd>g t</kbd> | |
| 150 | + | <kbd>g g</kbd> | |
| 151 | + | <kbd>G</kbd> | |
| 129 | 152 | </dt> | |
| 130 | - | <dd>go home / tree</dd> | |
| 153 | + | <dd>first / last row</dd> | |
| 131 | 154 | </div> | |
| 132 | 155 | <div> | |
| 133 | 156 | <dt> | |
| 134 | - | <kbd>g c</kbd> | |
| 135 | - | <kbd>g r</kbd> | |
| 157 | + | <kbd>/</kbd> | |
| 136 | 158 | </dt> | |
| 137 | - | <dd>go commits / refs</dd> | |
| 159 | + | <dd>filter the list</dd> | |
| 138 | 160 | </div> | |
| 139 | 161 | <div> | |
| 140 | 162 | <dt> | |
| 141 | 163 | <kbd>b</kbd> | |
| 142 | 164 | </dt> | |
| 143 | - | <dd>switch ref (branches, tags)</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> | |
| 165 | + | <dd>switch branch or tag</dd> | |
| 157 | 166 | </div> | |
| 158 | 167 | <div> | |
| 159 | 168 | <dt> | |
| @@ -161,15 +170,21 @@ | |||
| 161 | 170 | <kbd>t</kbd> | |
| 162 | 171 | <kbd>c</kbd> | |
| 163 | 172 | <kbd>r</kbd> | |
| 173 | + | <kbd>m</kbd> | |
| 164 | 174 | </dt> | |
| 165 | - | <dd>repo sections (when shown)</dd> | |
| 175 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 166 | 176 | </div> | |
| 167 | 177 | <div> | |
| 168 | 178 | <dt> | |
| 169 | - | <kbd>m</kbd> | |
| 170 | - | <kbd>g m</kbd> | |
| 179 | + | <kbd>g h</kbd> | |
| 171 | 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 | 188 | </div> | |
| 174 | 189 | <div> | |
| 175 | 190 | <dt> | |
| @@ -189,89 +204,92 @@ | |||
| 189 | 204 | <dt> | |
| 190 | 205 | <kbd>esc</kbd> | |
| 191 | 206 | </dt> | |
| 192 | - | <dd>close panel</dd> | |
| 207 | + | <dd>close a dialog, leave a field</dd> | |
| 193 | 208 | </div> | |
| 194 | 209 | </dl> | |
| 195 | 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 | 212 | <div class="panel-head"> | |
| 198 | - | <label for="cmd-q" class="hi">:</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(); | |
| 213 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 215 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 201 | 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 | 217 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 203 | 218 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 204 | 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 | 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 | 224 | </div> | |
| 208 | - | <ul id="cmd-list" class="cmd-list"> | |
| 225 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 209 | 226 | <li data-k="go home" data-cursor> | |
| 210 | 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 | 231 | </li> | |
| 213 | 232 | <li data-k="repo demo"> | |
| 214 | 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 | 237 | </li> | |
| 217 | 238 | <li data-k="repo quiet"> | |
| 218 | 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 | 243 | </li> | |
| 221 | 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 | 254 | </dialog> | |
| 223 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 255 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 242 | 256 | if (q('dialog[open]').count) return; | |
| 243 | 257 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 244 | 258 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 245 | 259 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 246 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 247 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 260 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 264 | })(1); | |
| 249 | - | k-k from:body -> ((d) => { | |
| 265 | + | k-k from:body -> (async (d) => { | |
| 250 | 266 | if (q('dialog[open]').count) return; | |
| 251 | 267 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 252 | 268 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 253 | 269 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 254 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 255 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 270 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 274 | })(-1); | |
| 257 | 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 | 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 | 277 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 260 | 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(); | |
| 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()); | |
| 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(); | |
| 279 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 282 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 265 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 266 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 267 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 268 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 269 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 270 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 271 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 272 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 273 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 274 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 283 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 284 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 285 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 286 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 287 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 288 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 289 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 290 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 291 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 292 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 275 | 293 | k-next from:body -> { | |
| 276 | 294 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 277 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>404 · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,35 +17,38 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| 28 | - | <span aria-current="page">-</span> | |
| 29 | - | </li> | |
| 30 | - | <li> | |
| 31 | - | <span aria-current="page">404</span> | |
| 32 | + | <span aria-current="page">not found</span> | |
| 32 | 33 | </li> | |
| 33 | 34 | </ol> | |
| 34 | 35 | </nav> | |
| 35 | - | <nav class="secs" aria-label="Sections"> | |
| 36 | - | <ul> | |
| 37 | - | <li> | |
| 38 | - | <a href="/" data-key="h" aria-keyshortcuts="g h">home</a> | |
| 39 | - | </li> | |
| 40 | - | </ul> | |
| 41 | - | </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> | |
| 42 | 46 | </header> | |
| 43 | 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 | 49 | <section class="state" aria-labelledby="state-title"> | |
| 46 | - | <h1 id="state-title"> | |
| 47 | - | <span class="code">404</span> not found</h1> | |
| 50 | + | <p class="eyebrow">404</p> | |
| 51 | + | <h1 id="state-title">not found</h1> | |
| 48 | 52 | <p class="dim">no such page, ref or path.</p> | |
| 49 | 53 | <p class="dim">check the URL, or start from the repo list.</p> | |
| 50 | 54 | <p class="state-back"> | |
| @@ -53,70 +57,71 @@ | |||
| 53 | 57 | </section> | |
| 54 | 58 | </main> | |
| 55 | 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 | 75 | <div class="panel-head"> | |
| 58 | - | <span>keys</span> | |
| 59 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 76 | + | <h2>Keyboard shortcuts</h2> | |
| 77 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 78 | + | <kbd>esc</kbd> close</button> | |
| 60 | 79 | </div> | |
| 61 | 80 | <dl class="keys"> | |
| 62 | 81 | <div> | |
| 63 | 82 | <dt> | |
| 64 | - | <kbd>j</kbd> | |
| 65 | - | <kbd>k</kbd> | |
| 83 | + | <kbd>ctrl k</kbd> | |
| 84 | + | <kbd>:</kbd> | |
| 66 | 85 | </dt> | |
| 67 | - | <dd>move cursor down / up</dd> | |
| 86 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 68 | 87 | </div> | |
| 69 | 88 | <div> | |
| 70 | 89 | <dt> | |
| 71 | - | <kbd>enter</kbd> | |
| 90 | + | <kbd>?</kbd> | |
| 72 | 91 | </dt> | |
| 73 | - | <dd>open row under cursor</dd> | |
| 92 | + | <dd>this list</dd> | |
| 74 | 93 | </div> | |
| 75 | 94 | <div> | |
| 76 | 95 | <dt> | |
| 77 | - | <kbd>g</kbd> | |
| 78 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 96 | + | <kbd>j</kbd> | |
| 97 | + | <kbd>k</kbd> | |
| 79 | 98 | </dt> | |
| 80 | - | <dd>first / last row</dd> | |
| 99 | + | <dd>move the row cursor down / up</dd> | |
| 81 | 100 | </div> | |
| 82 | 101 | <div> | |
| 83 | 102 | <dt> | |
| 84 | - | <kbd>/</kbd> | |
| 103 | + | <kbd>enter</kbd> | |
| 85 | 104 | </dt> | |
| 86 | - | <dd>focus the filter</dd> | |
| 105 | + | <dd>open the row under the cursor</dd> | |
| 87 | 106 | </div> | |
| 88 | 107 | <div> | |
| 89 | 108 | <dt> | |
| 90 | - | <kbd>g h</kbd> | |
| 91 | - | <kbd>g t</kbd> | |
| 109 | + | <kbd>g g</kbd> | |
| 110 | + | <kbd>G</kbd> | |
| 92 | 111 | </dt> | |
| 93 | - | <dd>go home / tree</dd> | |
| 112 | + | <dd>first / last row</dd> | |
| 94 | 113 | </div> | |
| 95 | 114 | <div> | |
| 96 | 115 | <dt> | |
| 97 | - | <kbd>g c</kbd> | |
| 98 | - | <kbd>g r</kbd> | |
| 116 | + | <kbd>/</kbd> | |
| 99 | 117 | </dt> | |
| 100 | - | <dd>go commits / refs</dd> | |
| 118 | + | <dd>filter the list</dd> | |
| 101 | 119 | </div> | |
| 102 | 120 | <div> | |
| 103 | 121 | <dt> | |
| 104 | 122 | <kbd>b</kbd> | |
| 105 | 123 | </dt> | |
| 106 | - | <dd>switch ref (branches, tags)</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> | |
| 124 | + | <dd>switch branch or tag</dd> | |
| 120 | 125 | </div> | |
| 121 | 126 | <div> | |
| 122 | 127 | <dt> | |
| @@ -124,15 +129,21 @@ | |||
| 124 | 129 | <kbd>t</kbd> | |
| 125 | 130 | <kbd>c</kbd> | |
| 126 | 131 | <kbd>r</kbd> | |
| 132 | + | <kbd>m</kbd> | |
| 127 | 133 | </dt> | |
| 128 | - | <dd>repo sections (when shown)</dd> | |
| 134 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 129 | 135 | </div> | |
| 130 | 136 | <div> | |
| 131 | 137 | <dt> | |
| 132 | - | <kbd>m</kbd> | |
| 133 | - | <kbd>g m</kbd> | |
| 138 | + | <kbd>g h</kbd> | |
| 139 | + | </dt> | |
| 140 | + | <dd>home</dd> | |
| 141 | + | </div> | |
| 142 | + | <div> | |
| 143 | + | <dt> | |
| 144 | + | <kbd>y</kbd> | |
| 134 | 145 | </dt> | |
| 135 | - | <dd>merge requests (when shown)</dd> | |
| 146 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 136 | 147 | </div> | |
| 137 | 148 | <div> | |
| 138 | 149 | <dt> | |
| @@ -152,77 +163,80 @@ | |||
| 152 | 163 | <dt> | |
| 153 | 164 | <kbd>esc</kbd> | |
| 154 | 165 | </dt> | |
| 155 | - | <dd>close panel</dd> | |
| 166 | + | <dd>close a dialog, leave a field</dd> | |
| 156 | 167 | </div> | |
| 157 | 168 | </dl> | |
| 158 | 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 | 171 | <div class="panel-head"> | |
| 161 | - | <label for="cmd-q" class="hi">:</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(); | |
| 172 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 174 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 164 | 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 | 176 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 166 | 177 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 167 | 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 | 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 | 183 | </div> | |
| 171 | - | <ul id="cmd-list" class="cmd-list"> | |
| 184 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 172 | 185 | <li data-k="go home" data-cursor> | |
| 173 | 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 | 190 | </li> | |
| 176 | 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 | 201 | </dialog> | |
| 178 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 202 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 193 | 203 | if (q('dialog[open]').count) return; | |
| 194 | 204 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 195 | 205 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 196 | 206 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 197 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 198 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 207 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 211 | })(1); | |
| 200 | - | k-k from:body -> ((d) => { | |
| 212 | + | k-k from:body -> (async (d) => { | |
| 201 | 213 | if (q('dialog[open]').count) return; | |
| 202 | 214 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 203 | 215 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 204 | 216 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 205 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 206 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 217 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 221 | })(-1); | |
| 208 | 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 | 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 | 224 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 211 | 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(); | |
| 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()); | |
| 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(); | |
| 226 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 229 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 216 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 217 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 218 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 219 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 220 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 221 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 222 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 223 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 224 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 225 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 230 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 231 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 232 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 233 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 234 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 235 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 236 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 237 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 238 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 239 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 226 | 240 | k-next from:body -> { | |
| 227 | 241 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 228 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>404 · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,35 +17,38 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 19 | </head> | |
| 19 | - | <body data-ref="" data-sha="" data-sel=""> | |
| 20 | + | <body data-ref="" data-sha="" data-sel="" data-nav=""> | |
| 20 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| 28 | - | <span aria-current="page">-</span> | |
| 29 | - | </li> | |
| 30 | - | <li> | |
| 31 | - | <span aria-current="page">404</span> | |
| 32 | + | <span aria-current="page">not found</span> | |
| 32 | 33 | </li> | |
| 33 | 34 | </ol> | |
| 34 | 35 | </nav> | |
| 35 | - | <nav class="secs" aria-label="Sections"> | |
| 36 | - | <ul> | |
| 37 | - | <li> | |
| 38 | - | <a href="/" data-key="h" aria-keyshortcuts="g h">home</a> | |
| 39 | - | </li> | |
| 40 | - | </ul> | |
| 41 | - | </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> | |
| 42 | 46 | </header> | |
| 43 | 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 | 49 | <section class="state" aria-labelledby="state-title"> | |
| 46 | - | <h1 id="state-title"> | |
| 47 | - | <span class="code">404</span> not found</h1> | |
| 50 | + | <p class="eyebrow">404</p> | |
| 51 | + | <h1 id="state-title">not found</h1> | |
| 48 | 52 | <p class="dim">no such page, ref or path.</p> | |
| 49 | 53 | <p class="dim">check the URL, or start from the repo list.</p> | |
| 50 | 54 | <p class="state-back"> | |
| @@ -53,70 +57,71 @@ | |||
| 53 | 57 | </section> | |
| 54 | 58 | </main> | |
| 55 | 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 | 75 | <div class="panel-head"> | |
| 58 | - | <span>keys</span> | |
| 59 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 76 | + | <h2>Keyboard shortcuts</h2> | |
| 77 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 78 | + | <kbd>esc</kbd> close</button> | |
| 60 | 79 | </div> | |
| 61 | 80 | <dl class="keys"> | |
| 62 | 81 | <div> | |
| 63 | 82 | <dt> | |
| 64 | - | <kbd>j</kbd> | |
| 65 | - | <kbd>k</kbd> | |
| 83 | + | <kbd>ctrl k</kbd> | |
| 84 | + | <kbd>:</kbd> | |
| 66 | 85 | </dt> | |
| 67 | - | <dd>move cursor down / up</dd> | |
| 86 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 68 | 87 | </div> | |
| 69 | 88 | <div> | |
| 70 | 89 | <dt> | |
| 71 | - | <kbd>enter</kbd> | |
| 90 | + | <kbd>?</kbd> | |
| 72 | 91 | </dt> | |
| 73 | - | <dd>open row under cursor</dd> | |
| 92 | + | <dd>this list</dd> | |
| 74 | 93 | </div> | |
| 75 | 94 | <div> | |
| 76 | 95 | <dt> | |
| 77 | - | <kbd>g</kbd> | |
| 78 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 96 | + | <kbd>j</kbd> | |
| 97 | + | <kbd>k</kbd> | |
| 79 | 98 | </dt> | |
| 80 | - | <dd>first / last row</dd> | |
| 99 | + | <dd>move the row cursor down / up</dd> | |
| 81 | 100 | </div> | |
| 82 | 101 | <div> | |
| 83 | 102 | <dt> | |
| 84 | - | <kbd>/</kbd> | |
| 103 | + | <kbd>enter</kbd> | |
| 85 | 104 | </dt> | |
| 86 | - | <dd>focus the filter</dd> | |
| 105 | + | <dd>open the row under the cursor</dd> | |
| 87 | 106 | </div> | |
| 88 | 107 | <div> | |
| 89 | 108 | <dt> | |
| 90 | - | <kbd>g h</kbd> | |
| 91 | - | <kbd>g t</kbd> | |
| 109 | + | <kbd>g g</kbd> | |
| 110 | + | <kbd>G</kbd> | |
| 92 | 111 | </dt> | |
| 93 | - | <dd>go home / tree</dd> | |
| 112 | + | <dd>first / last row</dd> | |
| 94 | 113 | </div> | |
| 95 | 114 | <div> | |
| 96 | 115 | <dt> | |
| 97 | - | <kbd>g c</kbd> | |
| 98 | - | <kbd>g r</kbd> | |
| 116 | + | <kbd>/</kbd> | |
| 99 | 117 | </dt> | |
| 100 | - | <dd>go commits / refs</dd> | |
| 118 | + | <dd>filter the list</dd> | |
| 101 | 119 | </div> | |
| 102 | 120 | <div> | |
| 103 | 121 | <dt> | |
| 104 | 122 | <kbd>b</kbd> | |
| 105 | 123 | </dt> | |
| 106 | - | <dd>switch ref (branches, tags)</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> | |
| 124 | + | <dd>switch branch or tag</dd> | |
| 120 | 125 | </div> | |
| 121 | 126 | <div> | |
| 122 | 127 | <dt> | |
| @@ -124,15 +129,21 @@ | |||
| 124 | 129 | <kbd>t</kbd> | |
| 125 | 130 | <kbd>c</kbd> | |
| 126 | 131 | <kbd>r</kbd> | |
| 132 | + | <kbd>m</kbd> | |
| 127 | 133 | </dt> | |
| 128 | - | <dd>repo sections (when shown)</dd> | |
| 134 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 129 | 135 | </div> | |
| 130 | 136 | <div> | |
| 131 | 137 | <dt> | |
| 132 | - | <kbd>m</kbd> | |
| 133 | - | <kbd>g m</kbd> | |
| 138 | + | <kbd>g h</kbd> | |
| 139 | + | </dt> | |
| 140 | + | <dd>home</dd> | |
| 141 | + | </div> | |
| 142 | + | <div> | |
| 143 | + | <dt> | |
| 144 | + | <kbd>y</kbd> | |
| 134 | 145 | </dt> | |
| 135 | - | <dd>merge requests (when shown)</dd> | |
| 146 | + | <dd>copy a permalink pinned to the commit</dd> | |
| 136 | 147 | </div> | |
| 137 | 148 | <div> | |
| 138 | 149 | <dt> | |
| @@ -152,77 +163,80 @@ | |||
| 152 | 163 | <dt> | |
| 153 | 164 | <kbd>esc</kbd> | |
| 154 | 165 | </dt> | |
| 155 | - | <dd>close panel</dd> | |
| 166 | + | <dd>close a dialog, leave a field</dd> | |
| 156 | 167 | </div> | |
| 157 | 168 | </dl> | |
| 158 | 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 | 171 | <div class="panel-head"> | |
| 161 | - | <label for="cmd-q" class="hi">:</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(); | |
| 172 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 174 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 164 | 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 | 176 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 166 | 177 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 167 | 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 | 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 | 183 | </div> | |
| 171 | - | <ul id="cmd-list" class="cmd-list"> | |
| 184 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 172 | 185 | <li data-k="go home" data-cursor> | |
| 173 | 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 | 190 | </li> | |
| 176 | 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 | 201 | </dialog> | |
| 178 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 202 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 193 | 203 | if (q('dialog[open]').count) return; | |
| 194 | 204 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 195 | 205 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 196 | 206 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 197 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 198 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 207 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 211 | })(1); | |
| 200 | - | k-k from:body -> ((d) => { | |
| 212 | + | k-k from:body -> (async (d) => { | |
| 201 | 213 | if (q('dialog[open]').count) return; | |
| 202 | 214 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 203 | 215 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 204 | 216 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 205 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 206 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 217 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 221 | })(-1); | |
| 208 | 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 | 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 | 224 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 211 | 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(); | |
| 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()); | |
| 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(); | |
| 226 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 229 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 216 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 217 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 218 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 219 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 220 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 221 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 222 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 223 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 224 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 225 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 230 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 231 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 232 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 233 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 234 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 235 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 236 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 237 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 238 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 239 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 226 | 240 | k-next from:body -> { | |
| 227 | 241 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 228 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>refs · demo · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -32,33 +36,52 @@ | |||
| 32 | 36 | </li> | |
| 33 | 37 | </ol> | |
| 34 | 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 | 56 | <ul> | |
| 37 | 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 | 60 | </li> | |
| 40 | 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 | 64 | </li> | |
| 43 | 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 | 68 | </li> | |
| 46 | 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 | 72 | </li> | |
| 49 | 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 | 76 | </li> | |
| 52 | 77 | </ul> | |
| 53 | 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 | 79 | </header> | |
| 57 | 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 | 82 | <div class="filter"> | |
| 60 | - | <label for="ref-filter">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)); | |
| 83 | + | <label for="ref-filter" class="sr">filter</label> | |
| 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 | 85 | let first = q('#ref-rows > tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#ref-rows > tr')"> | |
| 63 | 86 | </div> | |
| 64 | 87 | <table class="list refs" aria-label="Branches and tags"> | |
| @@ -80,7 +103,7 @@ | |||
| 80 | 103 | <td class="c-kind mute">branch</td> | |
| 81 | 104 | <td class="c-name"> | |
| 82 | 105 | <a href="/demo/tree/main">main</a> | |
| 83 | - | <span class="tagged">*</span> | |
| 106 | + | <span class="pill">default</span> | |
| 84 | 107 | </td> | |
| 85 | 108 | <td class="c-sha"> | |
| 86 | 109 | <a href="/demo/commit/SHA40">SHA7</a> | |
| @@ -139,10 +162,8 @@ | |||
| 139 | 162 | </table> | |
| 140 | 163 | </main> | |
| 141 | 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 | 165 | <section aria-labelledby="r-refs"> | |
| 145 | - | <h2 id="r-refs">refs</h2> | |
| 166 | + | <h2 id="r-refs">Overview</h2> | |
| 146 | 167 | <dl class="kv" id="refs-kv"> | |
| 147 | 168 | <dt>default</dt> | |
| 148 | 169 | <dd>main</dd> | |
| @@ -154,80 +175,78 @@ | |||
| 154 | 175 | <dd>v0.2.0</dd> | |
| 155 | 176 | </dl> | |
| 156 | 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 | 178 | </aside> | |
| 166 | 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 | 200 | <div class="panel-head"> | |
| 169 | - | <span>keys</span> | |
| 170 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 201 | + | <h2>Keyboard shortcuts</h2> | |
| 202 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 203 | + | <kbd>esc</kbd> close</button> | |
| 171 | 204 | </div> | |
| 172 | 205 | <dl class="keys"> | |
| 173 | 206 | <div> | |
| 174 | 207 | <dt> | |
| 175 | - | <kbd>j</kbd> | |
| 176 | - | <kbd>k</kbd> | |
| 208 | + | <kbd>ctrl k</kbd> | |
| 209 | + | <kbd>:</kbd> | |
| 177 | 210 | </dt> | |
| 178 | - | <dd>move cursor down / up</dd> | |
| 211 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 179 | 212 | </div> | |
| 180 | 213 | <div> | |
| 181 | 214 | <dt> | |
| 182 | - | <kbd>enter</kbd> | |
| 215 | + | <kbd>?</kbd> | |
| 183 | 216 | </dt> | |
| 184 | - | <dd>open row under cursor</dd> | |
| 217 | + | <dd>this list</dd> | |
| 185 | 218 | </div> | |
| 186 | 219 | <div> | |
| 187 | 220 | <dt> | |
| 188 | - | <kbd>g</kbd> | |
| 189 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 221 | + | <kbd>j</kbd> | |
| 222 | + | <kbd>k</kbd> | |
| 190 | 223 | </dt> | |
| 191 | - | <dd>first / last row</dd> | |
| 224 | + | <dd>move the row cursor down / up</dd> | |
| 192 | 225 | </div> | |
| 193 | 226 | <div> | |
| 194 | 227 | <dt> | |
| 195 | - | <kbd>/</kbd> | |
| 228 | + | <kbd>enter</kbd> | |
| 196 | 229 | </dt> | |
| 197 | - | <dd>focus the filter</dd> | |
| 230 | + | <dd>open the row under the cursor</dd> | |
| 198 | 231 | </div> | |
| 199 | 232 | <div> | |
| 200 | 233 | <dt> | |
| 201 | - | <kbd>g h</kbd> | |
| 202 | - | <kbd>g t</kbd> | |
| 234 | + | <kbd>g g</kbd> | |
| 235 | + | <kbd>G</kbd> | |
| 203 | 236 | </dt> | |
| 204 | - | <dd>go home / tree</dd> | |
| 237 | + | <dd>first / last row</dd> | |
| 205 | 238 | </div> | |
| 206 | 239 | <div> | |
| 207 | 240 | <dt> | |
| 208 | - | <kbd>g c</kbd> | |
| 209 | - | <kbd>g r</kbd> | |
| 241 | + | <kbd>/</kbd> | |
| 210 | 242 | </dt> | |
| 211 | - | <dd>go commits / refs</dd> | |
| 243 | + | <dd>filter the list</dd> | |
| 212 | 244 | </div> | |
| 213 | 245 | <div> | |
| 214 | 246 | <dt> | |
| 215 | 247 | <kbd>b</kbd> | |
| 216 | 248 | </dt> | |
| 217 | - | <dd>switch ref (branches, tags)</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> | |
| 249 | + | <dd>switch branch or tag</dd> | |
| 231 | 250 | </div> | |
| 232 | 251 | <div> | |
| 233 | 252 | <dt> | |
| @@ -235,15 +254,21 @@ | |||
| 235 | 254 | <kbd>t</kbd> | |
| 236 | 255 | <kbd>c</kbd> | |
| 237 | 256 | <kbd>r</kbd> | |
| 257 | + | <kbd>m</kbd> | |
| 238 | 258 | </dt> | |
| 239 | - | <dd>repo sections (when shown)</dd> | |
| 259 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 240 | 260 | </div> | |
| 241 | 261 | <div> | |
| 242 | 262 | <dt> | |
| 243 | - | <kbd>m</kbd> | |
| 244 | - | <kbd>g m</kbd> | |
| 263 | + | <kbd>g h</kbd> | |
| 245 | 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 | 272 | </div> | |
| 248 | 273 | <div> | |
| 249 | 274 | <dt> | |
| @@ -263,19 +288,21 @@ | |||
| 263 | 288 | <dt> | |
| 264 | 289 | <kbd>esc</kbd> | |
| 265 | 290 | </dt> | |
| 266 | - | <dd>close panel</dd> | |
| 291 | + | <dd>close a dialog, leave a field</dd> | |
| 267 | 292 | </div> | |
| 268 | 293 | </dl> | |
| 269 | 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 | 296 | <div class="panel-head"> | |
| 272 | - | <label for="refs-filter">ref></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))"> | |
| 274 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 297 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 299 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 300 | + | <kbd>esc</kbd> | |
| 301 | + | </button> | |
| 275 | 302 | </div> | |
| 276 | 303 | <div class="panel-cols" data-picklist> | |
| 277 | 304 | <section aria-label="Branches"> | |
| 278 | - | <h2>branches <span class="mute">2</span> | |
| 305 | + | <h2>Branches <span class="count">2</span> | |
| 279 | 306 | </h2> | |
| 280 | 307 | <ul> | |
| 281 | 308 | <li> | |
| @@ -289,7 +316,7 @@ | |||
| 289 | 316 | </ul> | |
| 290 | 317 | </section> | |
| 291 | 318 | <section aria-label="Tags"> | |
| 292 | - | <h2>tags <span class="mute">2</span> | |
| 319 | + | <h2>Tags <span class="count">2</span> | |
| 293 | 320 | </h2> | |
| 294 | 321 | <ul> | |
| 295 | 322 | <li> | |
| @@ -304,117 +331,136 @@ | |||
| 304 | 331 | </section> | |
| 305 | 332 | </div> | |
| 306 | 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 | 335 | <div class="panel-head"> | |
| 309 | - | <label for="cmd-q" class="hi">:</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(); | |
| 336 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 338 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 312 | 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 | 340 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 314 | 341 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 315 | 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 | 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 | 347 | </div> | |
| 319 | - | <ul id="cmd-list" class="cmd-list"> | |
| 348 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 320 | 349 | <li data-k="go home" data-cursor> | |
| 321 | 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 | 354 | </li> | |
| 324 | 355 | <li data-k="section summary"> | |
| 325 | 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 | 360 | </li> | |
| 328 | - | <li data-k="section tree"> | |
| 361 | + | <li data-k="section files"> | |
| 329 | 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 | 366 | </li> | |
| 332 | 367 | <li data-k="section commits"> | |
| 333 | 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 | 372 | </li> | |
| 336 | - | <li data-k="section refs"> | |
| 373 | + | <li data-k="section branches & tags"> | |
| 337 | 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 | 378 | </li> | |
| 340 | 379 | <li data-k="section merge requests"> | |
| 341 | 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 | 384 | </li> | |
| 344 | 385 | <li data-k="branch feature/x"> | |
| 345 | 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 | 390 | </li> | |
| 348 | 391 | <li data-k="branch main"> | |
| 349 | 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 | 396 | </li> | |
| 352 | 397 | <li data-k="tag v0.1.0"> | |
| 353 | 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 | 402 | </li> | |
| 356 | 403 | <li data-k="tag v0.2.0"> | |
| 357 | 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 | 408 | </li> | |
| 360 | 409 | <li data-k="repo quiet"> | |
| 361 | 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 | 414 | </li> | |
| 364 | 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 | 425 | </dialog> | |
| 366 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 426 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 385 | 427 | if (q('dialog[open]').count) return; | |
| 386 | 428 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 387 | 429 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 388 | 430 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 389 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 390 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 431 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 435 | })(1); | |
| 392 | - | k-k from:body -> ((d) => { | |
| 436 | + | k-k from:body -> (async (d) => { | |
| 393 | 437 | if (q('dialog[open]').count) return; | |
| 394 | 438 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 395 | 439 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 396 | 440 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 397 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 398 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 441 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 445 | })(-1); | |
| 400 | 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 | 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 | 448 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 403 | 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(); | |
| 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()); | |
| 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(); | |
| 450 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 453 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 408 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 409 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 410 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 411 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 412 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 413 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 414 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 415 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 416 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 417 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 454 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 455 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 456 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 457 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 458 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 459 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 460 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 461 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 462 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 463 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 418 | 464 | k-next from:body -> { | |
| 419 | 465 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 420 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>demo · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -29,34 +33,54 @@ | |||
| 29 | 33 | </li> | |
| 30 | 34 | </ol> | |
| 31 | 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 | 53 | <ul> | |
| 34 | 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 | 57 | </li> | |
| 37 | 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 | 61 | </li> | |
| 40 | 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 | 65 | </li> | |
| 43 | 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 | 69 | </li> | |
| 46 | 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 | 73 | </li> | |
| 49 | 74 | </ul> | |
| 50 | 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 | 76 | </header> | |
| 54 | - | <div class="work has-rail read"> | |
| 55 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | |
| 77 | + | <div class="work has-rail"> | |
| 78 | + | <main id="main" tabindex="-1"> | |
| 56 | 79 | <p class="lede">A demo repository</p> | |
| 57 | - | <h2 class="rule-h"> | |
| 58 | - | <span>README.md</span> | |
| 59 | - | </h2> | |
| 80 | + | <section class="card readme" aria-labelledby="readme-h"> | |
| 81 | + | <h2 class="card-h" id="readme-h"> | |
| 82 | + | <span class="ico i-file" aria-hidden="true"> | |
| 83 | + | </span> README.md</h2> | |
| 60 | 84 | <article class="prose" aria-label="README.md"> | |
| 61 | 85 | <h1 id="md-demo">Demo</h1> | |
| 62 | 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 | 105 | </tbody> | |
| 82 | 106 | </table> | |
| 83 | 107 | </article> | |
| 108 | + | </section> | |
| 84 | 109 | </main> | |
| 85 | 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 | 111 | <section aria-labelledby="r-clone"> | |
| 89 | - | <h2 id="r-clone">clone</h2> | |
| 112 | + | <h2 id="r-clone">Clone</h2> | |
| 90 | 113 | <div class="clone" id="clone-demo"> | |
| 91 | 114 | <div class="clone-line"> | |
| 92 | 115 | <span class="mute" aria-hidden="true">$ </span> | |
| 93 | 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 | 118 | </div> | |
| 96 | 119 | </div> | |
| 97 | 120 | </section> | |
| 98 | 121 | <section aria-labelledby="r-facts"> | |
| 99 | - | <h2 id="r-facts">facts</h2> | |
| 122 | + | <h2 id="r-facts">About</h2> | |
| 100 | 123 | <dl class="kv"> | |
| 101 | 124 | <dt>last push</dt> | |
| 102 | 125 | <dd id="last-push-demo"> | |
| @@ -113,7 +136,7 @@ | |||
| 113 | 136 | </dl> | |
| 114 | 137 | </section> | |
| 115 | 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 | 140 | </h2> | |
| 118 | 141 | <table class="list commits" aria-label="Commits"> | |
| 119 | 142 | <tbody data-rows> | |
| @@ -123,8 +146,9 @@ | |||
| 123 | 146 | <td class="c-sha mute">SHA7</td> | |
| 124 | 147 | <td class="c-subject"> | |
| 125 | 148 | <a href="/demo/commit/SHA40">add main.go</a> | |
| 126 | - | <span class="tagged">[main]</span> | |
| 127 | - | <span class="tagged">[tag: v0.2.0]</span> | |
| 149 | + | <span class="pill">main</span> | |
| 150 | + | <span class="pill tag"> | |
| 151 | + | <span class="sr">tag </span>v0.2.0</span> | |
| 128 | 152 | </td> | |
| 129 | 153 | <td class="c-who dim">T</td> | |
| 130 | 154 | <td class="c-when nowrap dim"> | |
| @@ -137,7 +161,8 @@ | |||
| 137 | 161 | <td class="c-sha mute">SHA7</td> | |
| 138 | 162 | <td class="c-subject"> | |
| 139 | 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 | 166 | </td> | |
| 142 | 167 | <td class="c-who dim">T</td> | |
| 143 | 168 | <td class="c-when nowrap dim"> | |
| @@ -148,80 +173,85 @@ | |||
| 148 | 173 | </table> | |
| 149 | 174 | </section> | |
| 150 | 175 | <section aria-labelledby="r-outline"> | |
| 151 | - | <h2 id="r-outline">README.md</h2> | |
| 176 | + | <h2 id="r-outline">On this page</h2> | |
| 152 | 177 | <nav class="outline" aria-label="Outline"> | |
| 153 | 178 | <a class="l1" href="#md-demo">Demo</a> | |
| 154 | 179 | <a class="l2" href="#md-install">Install</a> | |
| 155 | 180 | <a class="l2" href="#md-usage">Usage</a> | |
| 156 | 181 | </nav> | |
| 157 | 182 | </section> | |
| 158 | - | </details> | |
| 159 | 183 | </aside> | |
| 160 | 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 | 205 | <div class="panel-head"> | |
| 163 | - | <span>keys</span> | |
| 164 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 206 | + | <h2>Keyboard shortcuts</h2> | |
| 207 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 208 | + | <kbd>esc</kbd> close</button> | |
| 165 | 209 | </div> | |
| 166 | 210 | <dl class="keys"> | |
| 167 | 211 | <div> | |
| 168 | 212 | <dt> | |
| 169 | - | <kbd>j</kbd> | |
| 170 | - | <kbd>k</kbd> | |
| 213 | + | <kbd>ctrl k</kbd> | |
| 214 | + | <kbd>:</kbd> | |
| 171 | 215 | </dt> | |
| 172 | - | <dd>move cursor down / up</dd> | |
| 216 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 173 | 217 | </div> | |
| 174 | 218 | <div> | |
| 175 | 219 | <dt> | |
| 176 | - | <kbd>enter</kbd> | |
| 220 | + | <kbd>?</kbd> | |
| 177 | 221 | </dt> | |
| 178 | - | <dd>open row under cursor</dd> | |
| 222 | + | <dd>this list</dd> | |
| 179 | 223 | </div> | |
| 180 | 224 | <div> | |
| 181 | 225 | <dt> | |
| 182 | - | <kbd>g</kbd> | |
| 183 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 226 | + | <kbd>j</kbd> | |
| 227 | + | <kbd>k</kbd> | |
| 184 | 228 | </dt> | |
| 185 | - | <dd>first / last row</dd> | |
| 229 | + | <dd>move the row cursor down / up</dd> | |
| 186 | 230 | </div> | |
| 187 | 231 | <div> | |
| 188 | 232 | <dt> | |
| 189 | - | <kbd>/</kbd> | |
| 233 | + | <kbd>enter</kbd> | |
| 190 | 234 | </dt> | |
| 191 | - | <dd>focus the filter</dd> | |
| 235 | + | <dd>open the row under the cursor</dd> | |
| 192 | 236 | </div> | |
| 193 | 237 | <div> | |
| 194 | 238 | <dt> | |
| 195 | - | <kbd>g h</kbd> | |
| 196 | - | <kbd>g t</kbd> | |
| 239 | + | <kbd>g g</kbd> | |
| 240 | + | <kbd>G</kbd> | |
| 197 | 241 | </dt> | |
| 198 | - | <dd>go home / tree</dd> | |
| 242 | + | <dd>first / last row</dd> | |
| 199 | 243 | </div> | |
| 200 | 244 | <div> | |
| 201 | 245 | <dt> | |
| 202 | - | <kbd>g c</kbd> | |
| 203 | - | <kbd>g r</kbd> | |
| 246 | + | <kbd>/</kbd> | |
| 204 | 247 | </dt> | |
| 205 | - | <dd>go commits / refs</dd> | |
| 248 | + | <dd>filter the list</dd> | |
| 206 | 249 | </div> | |
| 207 | 250 | <div> | |
| 208 | 251 | <dt> | |
| 209 | 252 | <kbd>b</kbd> | |
| 210 | 253 | </dt> | |
| 211 | - | <dd>switch ref (branches, tags)</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> | |
| 254 | + | <dd>switch branch or tag</dd> | |
| 225 | 255 | </div> | |
| 226 | 256 | <div> | |
| 227 | 257 | <dt> | |
| @@ -229,15 +259,21 @@ | |||
| 229 | 259 | <kbd>t</kbd> | |
| 230 | 260 | <kbd>c</kbd> | |
| 231 | 261 | <kbd>r</kbd> | |
| 262 | + | <kbd>m</kbd> | |
| 232 | 263 | </dt> | |
| 233 | - | <dd>repo sections (when shown)</dd> | |
| 264 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 234 | 265 | </div> | |
| 235 | 266 | <div> | |
| 236 | 267 | <dt> | |
| 237 | - | <kbd>m</kbd> | |
| 238 | - | <kbd>g m</kbd> | |
| 268 | + | <kbd>g h</kbd> | |
| 239 | 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 | 277 | </div> | |
| 242 | 278 | <div> | |
| 243 | 279 | <dt> | |
| @@ -257,19 +293,21 @@ | |||
| 257 | 293 | <dt> | |
| 258 | 294 | <kbd>esc</kbd> | |
| 259 | 295 | </dt> | |
| 260 | - | <dd>close panel</dd> | |
| 296 | + | <dd>close a dialog, leave a field</dd> | |
| 261 | 297 | </div> | |
| 262 | 298 | </dl> | |
| 263 | 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 | 301 | <div class="panel-head"> | |
| 266 | - | <label for="refs-filter">ref></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))"> | |
| 268 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 302 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 304 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 305 | + | <kbd>esc</kbd> | |
| 306 | + | </button> | |
| 269 | 307 | </div> | |
| 270 | 308 | <div class="panel-cols" data-picklist> | |
| 271 | 309 | <section aria-label="Branches"> | |
| 272 | - | <h2>branches <span class="mute">2</span> | |
| 310 | + | <h2>Branches <span class="count">2</span> | |
| 273 | 311 | </h2> | |
| 274 | 312 | <ul> | |
| 275 | 313 | <li> | |
| @@ -283,7 +321,7 @@ | |||
| 283 | 321 | </ul> | |
| 284 | 322 | </section> | |
| 285 | 323 | <section aria-label="Tags"> | |
| 286 | - | <h2>tags <span class="mute">2</span> | |
| 324 | + | <h2>Tags <span class="count">2</span> | |
| 287 | 325 | </h2> | |
| 288 | 326 | <ul> | |
| 289 | 327 | <li> | |
| @@ -298,117 +336,136 @@ | |||
| 298 | 336 | </section> | |
| 299 | 337 | </div> | |
| 300 | 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 | 340 | <div class="panel-head"> | |
| 303 | - | <label for="cmd-q" class="hi">:</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(); | |
| 341 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 343 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 306 | 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 | 345 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 308 | 346 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 309 | 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 | 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 | 352 | </div> | |
| 313 | - | <ul id="cmd-list" class="cmd-list"> | |
| 353 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 314 | 354 | <li data-k="go home" data-cursor> | |
| 315 | 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 | 359 | </li> | |
| 318 | 360 | <li data-k="section summary"> | |
| 319 | 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 | 365 | </li> | |
| 322 | - | <li data-k="section tree"> | |
| 366 | + | <li data-k="section files"> | |
| 323 | 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 | 371 | </li> | |
| 326 | 372 | <li data-k="section commits"> | |
| 327 | 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 | 377 | </li> | |
| 330 | - | <li data-k="section refs"> | |
| 378 | + | <li data-k="section branches & tags"> | |
| 331 | 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 | 383 | </li> | |
| 334 | 384 | <li data-k="section merge requests"> | |
| 335 | 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 | 389 | </li> | |
| 338 | 390 | <li data-k="branch feature/x"> | |
| 339 | 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 | 395 | </li> | |
| 342 | 396 | <li data-k="branch main"> | |
| 343 | 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 | 401 | </li> | |
| 346 | 402 | <li data-k="tag v0.1.0"> | |
| 347 | 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 | 407 | </li> | |
| 350 | 408 | <li data-k="tag v0.2.0"> | |
| 351 | 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 | 413 | </li> | |
| 354 | 414 | <li data-k="repo quiet"> | |
| 355 | 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 | 419 | </li> | |
| 358 | 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 | 430 | </dialog> | |
| 360 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 431 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 379 | 432 | if (q('dialog[open]').count) return; | |
| 380 | 433 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 381 | 434 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 382 | 435 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 383 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 384 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 436 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 440 | })(1); | |
| 386 | - | k-k from:body -> ((d) => { | |
| 441 | + | k-k from:body -> (async (d) => { | |
| 387 | 442 | if (q('dialog[open]').count) return; | |
| 388 | 443 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 389 | 444 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 390 | 445 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 391 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 392 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 446 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 450 | })(-1); | |
| 394 | 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 | 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 | 453 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 397 | 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(); | |
| 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()); | |
| 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(); | |
| 455 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 458 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 402 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 403 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 404 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 405 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 406 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 407 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 408 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 409 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 410 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 411 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 459 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 460 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 461 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 462 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 463 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 464 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 465 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 466 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 467 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 468 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 412 | 469 | k-next from:body -> { | |
| 413 | 470 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 414 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>src/lib · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -41,33 +45,52 @@ | |||
| 41 | 45 | </li> | |
| 42 | 46 | </ol> | |
| 43 | 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 | 65 | <ul> | |
| 46 | 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 | 69 | </li> | |
| 49 | 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 | 73 | </li> | |
| 52 | 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 | 77 | </li> | |
| 55 | 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 | 81 | </li> | |
| 58 | 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 | 85 | </li> | |
| 61 | 86 | </ul> | |
| 62 | 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 | 88 | </header> | |
| 66 | 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 | 91 | <div class="filter"> | |
| 69 | - | <label for="tree-filter">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])')"> | |
| 92 | + | <label for="tree-filter" class="sr">filter</label> | |
| 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 | 94 | </div> | |
| 72 | 95 | <table class="list tree" aria-label="Entries"> | |
| 73 | 96 | <thead> | |
| @@ -104,70 +127,71 @@ | |||
| 104 | 127 | </table> | |
| 105 | 128 | </main> | |
| 106 | 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 | 145 | <div class="panel-head"> | |
| 109 | - | <span>keys</span> | |
| 110 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 146 | + | <h2>Keyboard shortcuts</h2> | |
| 147 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 148 | + | <kbd>esc</kbd> close</button> | |
| 111 | 149 | </div> | |
| 112 | 150 | <dl class="keys"> | |
| 113 | 151 | <div> | |
| 114 | 152 | <dt> | |
| 115 | - | <kbd>j</kbd> | |
| 116 | - | <kbd>k</kbd> | |
| 153 | + | <kbd>ctrl k</kbd> | |
| 154 | + | <kbd>:</kbd> | |
| 117 | 155 | </dt> | |
| 118 | - | <dd>move cursor down / up</dd> | |
| 156 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 119 | 157 | </div> | |
| 120 | 158 | <div> | |
| 121 | 159 | <dt> | |
| 122 | - | <kbd>enter</kbd> | |
| 160 | + | <kbd>?</kbd> | |
| 123 | 161 | </dt> | |
| 124 | - | <dd>open row under cursor</dd> | |
| 162 | + | <dd>this list</dd> | |
| 125 | 163 | </div> | |
| 126 | 164 | <div> | |
| 127 | 165 | <dt> | |
| 128 | - | <kbd>g</kbd> | |
| 129 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 166 | + | <kbd>j</kbd> | |
| 167 | + | <kbd>k</kbd> | |
| 130 | 168 | </dt> | |
| 131 | - | <dd>first / last row</dd> | |
| 169 | + | <dd>move the row cursor down / up</dd> | |
| 132 | 170 | </div> | |
| 133 | 171 | <div> | |
| 134 | 172 | <dt> | |
| 135 | - | <kbd>/</kbd> | |
| 173 | + | <kbd>enter</kbd> | |
| 136 | 174 | </dt> | |
| 137 | - | <dd>focus the filter</dd> | |
| 175 | + | <dd>open the row under the cursor</dd> | |
| 138 | 176 | </div> | |
| 139 | 177 | <div> | |
| 140 | 178 | <dt> | |
| 141 | - | <kbd>g h</kbd> | |
| 142 | - | <kbd>g t</kbd> | |
| 179 | + | <kbd>g g</kbd> | |
| 180 | + | <kbd>G</kbd> | |
| 143 | 181 | </dt> | |
| 144 | - | <dd>go home / tree</dd> | |
| 182 | + | <dd>first / last row</dd> | |
| 145 | 183 | </div> | |
| 146 | 184 | <div> | |
| 147 | 185 | <dt> | |
| 148 | - | <kbd>g c</kbd> | |
| 149 | - | <kbd>g r</kbd> | |
| 186 | + | <kbd>/</kbd> | |
| 150 | 187 | </dt> | |
| 151 | - | <dd>go commits / refs</dd> | |
| 188 | + | <dd>filter the list</dd> | |
| 152 | 189 | </div> | |
| 153 | 190 | <div> | |
| 154 | 191 | <dt> | |
| 155 | 192 | <kbd>b</kbd> | |
| 156 | 193 | </dt> | |
| 157 | - | <dd>switch ref (branches, tags)</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> | |
| 194 | + | <dd>switch branch or tag</dd> | |
| 171 | 195 | </div> | |
| 172 | 196 | <div> | |
| 173 | 197 | <dt> | |
| @@ -175,15 +199,21 @@ | |||
| 175 | 199 | <kbd>t</kbd> | |
| 176 | 200 | <kbd>c</kbd> | |
| 177 | 201 | <kbd>r</kbd> | |
| 202 | + | <kbd>m</kbd> | |
| 178 | 203 | </dt> | |
| 179 | - | <dd>repo sections (when shown)</dd> | |
| 204 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 180 | 205 | </div> | |
| 181 | 206 | <div> | |
| 182 | 207 | <dt> | |
| 183 | - | <kbd>m</kbd> | |
| 184 | - | <kbd>g m</kbd> | |
| 208 | + | <kbd>g h</kbd> | |
| 185 | 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 | 217 | </div> | |
| 188 | 218 | <div> | |
| 189 | 219 | <dt> | |
| @@ -203,19 +233,21 @@ | |||
| 203 | 233 | <dt> | |
| 204 | 234 | <kbd>esc</kbd> | |
| 205 | 235 | </dt> | |
| 206 | - | <dd>close panel</dd> | |
| 236 | + | <dd>close a dialog, leave a field</dd> | |
| 207 | 237 | </div> | |
| 208 | 238 | </dl> | |
| 209 | 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 | 241 | <div class="panel-head"> | |
| 212 | - | <label for="refs-filter">ref></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))"> | |
| 214 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 242 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 244 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 245 | + | <kbd>esc</kbd> | |
| 246 | + | </button> | |
| 215 | 247 | </div> | |
| 216 | 248 | <div class="panel-cols" data-picklist> | |
| 217 | 249 | <section aria-label="Branches"> | |
| 218 | - | <h2>branches <span class="mute">2</span> | |
| 250 | + | <h2>Branches <span class="count">2</span> | |
| 219 | 251 | </h2> | |
| 220 | 252 | <ul> | |
| 221 | 253 | <li> | |
| @@ -229,7 +261,7 @@ | |||
| 229 | 261 | </ul> | |
| 230 | 262 | </section> | |
| 231 | 263 | <section aria-label="Tags"> | |
| 232 | - | <h2>tags <span class="mute">1</span> | |
| 264 | + | <h2>Tags <span class="count">1</span> | |
| 233 | 265 | </h2> | |
| 234 | 266 | <ul> | |
| 235 | 267 | <li> | |
| @@ -240,110 +272,130 @@ | |||
| 240 | 272 | </section> | |
| 241 | 273 | </div> | |
| 242 | 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 | 276 | <div class="panel-head"> | |
| 245 | - | <label for="cmd-q" class="hi">:</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(); | |
| 277 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 279 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 248 | 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 | 281 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 250 | 282 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 251 | 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 | 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 | 288 | </div> | |
| 255 | - | <ul id="cmd-list" class="cmd-list"> | |
| 289 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 256 | 290 | <li data-k="go home" data-cursor> | |
| 257 | 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 | 295 | </li> | |
| 260 | 296 | <li data-k="section summary"> | |
| 261 | 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 | 301 | </li> | |
| 264 | - | <li data-k="section tree"> | |
| 302 | + | <li data-k="section files"> | |
| 265 | 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 | 307 | </li> | |
| 268 | 308 | <li data-k="section commits"> | |
| 269 | 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 | 313 | </li> | |
| 272 | - | <li data-k="section refs"> | |
| 314 | + | <li data-k="section branches & tags"> | |
| 273 | 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 | 319 | </li> | |
| 276 | 320 | <li data-k="section merge requests"> | |
| 277 | 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 | 325 | </li> | |
| 280 | 326 | <li data-k="branch feature/x"> | |
| 281 | 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 | 331 | </li> | |
| 284 | 332 | <li data-k="branch main"> | |
| 285 | 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 | 337 | </li> | |
| 288 | 338 | <li data-k="tag v1.0"> | |
| 289 | 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 | 343 | </li> | |
| 292 | 344 | <li data-k="file src/lib/util.go"> | |
| 293 | 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 | 349 | </li> | |
| 296 | 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 | 360 | </dialog> | |
| 298 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 361 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 314 | 362 | if (q('dialog[open]').count) return; | |
| 315 | 363 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 316 | 364 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 317 | 365 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 318 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 319 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 366 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 370 | })(1); | |
| 321 | - | k-k from:body -> ((d) => { | |
| 371 | + | k-k from:body -> (async (d) => { | |
| 322 | 372 | if (q('dialog[open]').count) return; | |
| 323 | 373 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 324 | 374 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 325 | 375 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 326 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 327 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 376 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 380 | })(-1); | |
| 329 | 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 | 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 | 383 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 332 | 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(); | |
| 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()); | |
| 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(); | |
| 385 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 388 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 337 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 338 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 339 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 340 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 341 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 342 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 343 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 344 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 345 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 346 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 389 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 390 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 391 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 392 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 393 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 394 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 395 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 396 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 397 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 398 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 347 | 399 | k-next from:body -> { | |
| 348 | 400 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 349 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>tree · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -35,33 +39,52 @@ | |||
| 35 | 39 | </li> | |
| 36 | 40 | </ol> | |
| 37 | 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 | 59 | <ul> | |
| 40 | 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 | 63 | </li> | |
| 43 | 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 | 67 | </li> | |
| 46 | 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 | 71 | </li> | |
| 49 | 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 | 75 | </li> | |
| 52 | 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 | 79 | </li> | |
| 55 | 80 | </ul> | |
| 56 | 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 | 82 | </header> | |
| 60 | 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 | 85 | <div class="filter"> | |
| 63 | - | <label for="tree-filter">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])')"> | |
| 86 | + | <label for="tree-filter" class="sr">filter</label> | |
| 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 | 88 | </div> | |
| 66 | 89 | <table class="list tree" aria-label="Entries"> | |
| 67 | 90 | <thead> | |
| @@ -320,79 +343,82 @@ | |||
| 320 | 343 | </tr> | |
| 321 | 344 | </tbody> | |
| 322 | 345 | </table> | |
| 323 | - | <h2 class="rule-h"> | |
| 324 | - | <span>README.md</span> | |
| 325 | - | </h2> | |
| 346 | + | <section class="card readme" aria-labelledby="readme-h"> | |
| 347 | + | <h2 class="card-h" id="readme-h"> | |
| 348 | + | <span class="ico i-file" aria-hidden="true"> | |
| 349 | + | </span> README.md</h2> | |
| 326 | 350 | <article class="prose" aria-label="README.md"> | |
| 327 | 351 | <h1 id="md-files">Files</h1> | |
| 328 | 352 | <p>See <a href="/files/blob/main/docs/guide.md">the guide</a>.</p> | |
| 329 | 353 | </article> | |
| 354 | + | </section> | |
| 330 | 355 | </main> | |
| 331 | 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 | 372 | <div class="panel-head"> | |
| 334 | - | <span>keys</span> | |
| 335 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 373 | + | <h2>Keyboard shortcuts</h2> | |
| 374 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 375 | + | <kbd>esc</kbd> close</button> | |
| 336 | 376 | </div> | |
| 337 | 377 | <dl class="keys"> | |
| 338 | 378 | <div> | |
| 339 | 379 | <dt> | |
| 340 | - | <kbd>j</kbd> | |
| 341 | - | <kbd>k</kbd> | |
| 380 | + | <kbd>ctrl k</kbd> | |
| 381 | + | <kbd>:</kbd> | |
| 342 | 382 | </dt> | |
| 343 | - | <dd>move cursor down / up</dd> | |
| 383 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 344 | 384 | </div> | |
| 345 | 385 | <div> | |
| 346 | 386 | <dt> | |
| 347 | - | <kbd>enter</kbd> | |
| 387 | + | <kbd>?</kbd> | |
| 348 | 388 | </dt> | |
| 349 | - | <dd>open row under cursor</dd> | |
| 389 | + | <dd>this list</dd> | |
| 350 | 390 | </div> | |
| 351 | 391 | <div> | |
| 352 | 392 | <dt> | |
| 353 | - | <kbd>g</kbd> | |
| 354 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 393 | + | <kbd>j</kbd> | |
| 394 | + | <kbd>k</kbd> | |
| 355 | 395 | </dt> | |
| 356 | - | <dd>first / last row</dd> | |
| 396 | + | <dd>move the row cursor down / up</dd> | |
| 357 | 397 | </div> | |
| 358 | 398 | <div> | |
| 359 | 399 | <dt> | |
| 360 | - | <kbd>/</kbd> | |
| 400 | + | <kbd>enter</kbd> | |
| 361 | 401 | </dt> | |
| 362 | - | <dd>focus the filter</dd> | |
| 402 | + | <dd>open the row under the cursor</dd> | |
| 363 | 403 | </div> | |
| 364 | 404 | <div> | |
| 365 | 405 | <dt> | |
| 366 | - | <kbd>g h</kbd> | |
| 367 | - | <kbd>g t</kbd> | |
| 406 | + | <kbd>g g</kbd> | |
| 407 | + | <kbd>G</kbd> | |
| 368 | 408 | </dt> | |
| 369 | - | <dd>go home / tree</dd> | |
| 409 | + | <dd>first / last row</dd> | |
| 370 | 410 | </div> | |
| 371 | 411 | <div> | |
| 372 | 412 | <dt> | |
| 373 | - | <kbd>g c</kbd> | |
| 374 | - | <kbd>g r</kbd> | |
| 413 | + | <kbd>/</kbd> | |
| 375 | 414 | </dt> | |
| 376 | - | <dd>go commits / refs</dd> | |
| 415 | + | <dd>filter the list</dd> | |
| 377 | 416 | </div> | |
| 378 | 417 | <div> | |
| 379 | 418 | <dt> | |
| 380 | 419 | <kbd>b</kbd> | |
| 381 | 420 | </dt> | |
| 382 | - | <dd>switch ref (branches, tags)</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> | |
| 421 | + | <dd>switch branch or tag</dd> | |
| 396 | 422 | </div> | |
| 397 | 423 | <div> | |
| 398 | 424 | <dt> | |
| @@ -400,15 +426,21 @@ | |||
| 400 | 426 | <kbd>t</kbd> | |
| 401 | 427 | <kbd>c</kbd> | |
| 402 | 428 | <kbd>r</kbd> | |
| 429 | + | <kbd>m</kbd> | |
| 403 | 430 | </dt> | |
| 404 | - | <dd>repo sections (when shown)</dd> | |
| 431 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 405 | 432 | </div> | |
| 406 | 433 | <div> | |
| 407 | 434 | <dt> | |
| 408 | - | <kbd>m</kbd> | |
| 409 | - | <kbd>g m</kbd> | |
| 435 | + | <kbd>g h</kbd> | |
| 410 | 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 | 444 | </div> | |
| 413 | 445 | <div> | |
| 414 | 446 | <dt> | |
| @@ -428,19 +460,21 @@ | |||
| 428 | 460 | <dt> | |
| 429 | 461 | <kbd>esc</kbd> | |
| 430 | 462 | </dt> | |
| 431 | - | <dd>close panel</dd> | |
| 463 | + | <dd>close a dialog, leave a field</dd> | |
| 432 | 464 | </div> | |
| 433 | 465 | </dl> | |
| 434 | 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 | 468 | <div class="panel-head"> | |
| 437 | - | <label for="refs-filter">ref></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))"> | |
| 439 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 469 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 471 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 472 | + | <kbd>esc</kbd> | |
| 473 | + | </button> | |
| 440 | 474 | </div> | |
| 441 | 475 | <div class="panel-cols" data-picklist> | |
| 442 | 476 | <section aria-label="Branches"> | |
| 443 | - | <h2>branches <span class="mute">2</span> | |
| 477 | + | <h2>Branches <span class="count">2</span> | |
| 444 | 478 | </h2> | |
| 445 | 479 | <ul> | |
| 446 | 480 | <li> | |
| @@ -454,7 +488,7 @@ | |||
| 454 | 488 | </ul> | |
| 455 | 489 | </section> | |
| 456 | 490 | <section aria-label="Tags"> | |
| 457 | - | <h2>tags <span class="mute">1</span> | |
| 491 | + | <h2>Tags <span class="count">1</span> | |
| 458 | 492 | </h2> | |
| 459 | 493 | <ul> | |
| 460 | 494 | <li> | |
| @@ -465,158 +499,202 @@ | |||
| 465 | 499 | </section> | |
| 466 | 500 | </div> | |
| 467 | 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 | 503 | <div class="panel-head"> | |
| 470 | - | <label for="cmd-q" class="hi">:</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(); | |
| 504 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 506 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 473 | 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 | 508 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 475 | 509 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 476 | 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 | 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 | 515 | </div> | |
| 480 | - | <ul id="cmd-list" class="cmd-list"> | |
| 516 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 481 | 517 | <li data-k="go home" data-cursor> | |
| 482 | 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 | 522 | </li> | |
| 485 | 523 | <li data-k="section summary"> | |
| 486 | 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 | 528 | </li> | |
| 489 | - | <li data-k="section tree"> | |
| 529 | + | <li data-k="section files"> | |
| 490 | 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 | 534 | </li> | |
| 493 | 535 | <li data-k="section commits"> | |
| 494 | 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 | 540 | </li> | |
| 497 | - | <li data-k="section refs"> | |
| 541 | + | <li data-k="section branches & tags"> | |
| 498 | 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 | 546 | </li> | |
| 501 | 547 | <li data-k="section merge requests"> | |
| 502 | 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 | 552 | </li> | |
| 505 | 553 | <li data-k="branch feature/x"> | |
| 506 | 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 | 558 | </li> | |
| 509 | 559 | <li data-k="branch main"> | |
| 510 | 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 | 564 | </li> | |
| 513 | 565 | <li data-k="tag v1.0"> | |
| 514 | 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 | 570 | </li> | |
| 517 | 571 | <li data-k="file readme.md"> | |
| 518 | 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 | 576 | </li> | |
| 521 | 577 | <li data-k="file big.txt"> | |
| 522 | 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 | 582 | </li> | |
| 525 | 583 | <li data-k="file data.bin"> | |
| 526 | 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 | 588 | </li> | |
| 529 | 589 | <li data-k="file empty.txt"> | |
| 530 | 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 | 594 | </li> | |
| 533 | 595 | <li data-k="file evil.go"> | |
| 534 | 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 | 600 | </li> | |
| 537 | 601 | <li data-k="file fake.jpg"> | |
| 538 | 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 | 606 | </li> | |
| 541 | 607 | <li data-k="file fake.png"> | |
| 542 | 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 | 612 | </li> | |
| 545 | 613 | <li data-k="file lines.txt"> | |
| 546 | 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 | 618 | </li> | |
| 549 | 619 | <li data-k="file logo.svg"> | |
| 550 | 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 | 624 | </li> | |
| 553 | 625 | <li data-k="file main.go"> | |
| 554 | 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 | 630 | </li> | |
| 557 | 631 | <li data-k="file notes.txt"> | |
| 558 | 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 | 636 | </li> | |
| 561 | 637 | <li data-k="file page.html"> | |
| 562 | 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 | 642 | </li> | |
| 565 | 643 | <li data-k="file pic.png"> | |
| 566 | 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 | 648 | </li> | |
| 569 | 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 | 659 | </dialog> | |
| 571 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 660 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 587 | 661 | if (q('dialog[open]').count) return; | |
| 588 | 662 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 589 | 663 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 590 | 664 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 591 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 592 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 665 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 669 | })(1); | |
| 594 | - | k-k from:body -> ((d) => { | |
| 670 | + | k-k from:body -> (async (d) => { | |
| 595 | 671 | if (q('dialog[open]').count) return; | |
| 596 | 672 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 597 | 673 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 598 | 674 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 599 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 600 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 675 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 679 | })(-1); | |
| 602 | 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 | 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 | 682 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 605 | 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(); | |
| 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()); | |
| 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(); | |
| 684 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 687 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 610 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 611 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 612 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 613 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 614 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 615 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 616 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 617 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 618 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 619 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 688 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 689 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 690 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 691 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 692 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 693 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 694 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 695 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 696 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 697 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 620 | 698 | k-next from:body -> { | |
| 621 | 699 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 622 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>x · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,33 +42,52 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <div class="filter"> | |
| 66 | - | <label for="tree-filter">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])')"> | |
| 89 | + | <label for="tree-filter" class="sr">filter</label> | |
| 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 | 91 | </div> | |
| 69 | 92 | <table class="list tree" aria-label="Entries"> | |
| 70 | 93 | <thead> | |
| @@ -101,70 +124,71 @@ | |||
| 101 | 124 | </table> | |
| 102 | 125 | </main> | |
| 103 | 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 | 142 | <div class="panel-head"> | |
| 106 | - | <span>keys</span> | |
| 107 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 143 | + | <h2>Keyboard shortcuts</h2> | |
| 144 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 145 | + | <kbd>esc</kbd> close</button> | |
| 108 | 146 | </div> | |
| 109 | 147 | <dl class="keys"> | |
| 110 | 148 | <div> | |
| 111 | 149 | <dt> | |
| 112 | - | <kbd>j</kbd> | |
| 113 | - | <kbd>k</kbd> | |
| 150 | + | <kbd>ctrl k</kbd> | |
| 151 | + | <kbd>:</kbd> | |
| 114 | 152 | </dt> | |
| 115 | - | <dd>move cursor down / up</dd> | |
| 153 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 116 | 154 | </div> | |
| 117 | 155 | <div> | |
| 118 | 156 | <dt> | |
| 119 | - | <kbd>enter</kbd> | |
| 157 | + | <kbd>?</kbd> | |
| 120 | 158 | </dt> | |
| 121 | - | <dd>open row under cursor</dd> | |
| 159 | + | <dd>this list</dd> | |
| 122 | 160 | </div> | |
| 123 | 161 | <div> | |
| 124 | 162 | <dt> | |
| 125 | - | <kbd>g</kbd> | |
| 126 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 163 | + | <kbd>j</kbd> | |
| 164 | + | <kbd>k</kbd> | |
| 127 | 165 | </dt> | |
| 128 | - | <dd>first / last row</dd> | |
| 166 | + | <dd>move the row cursor down / up</dd> | |
| 129 | 167 | </div> | |
| 130 | 168 | <div> | |
| 131 | 169 | <dt> | |
| 132 | - | <kbd>/</kbd> | |
| 170 | + | <kbd>enter</kbd> | |
| 133 | 171 | </dt> | |
| 134 | - | <dd>focus the filter</dd> | |
| 172 | + | <dd>open the row under the cursor</dd> | |
| 135 | 173 | </div> | |
| 136 | 174 | <div> | |
| 137 | 175 | <dt> | |
| 138 | - | <kbd>g h</kbd> | |
| 139 | - | <kbd>g t</kbd> | |
| 176 | + | <kbd>g g</kbd> | |
| 177 | + | <kbd>G</kbd> | |
| 140 | 178 | </dt> | |
| 141 | - | <dd>go home / tree</dd> | |
| 179 | + | <dd>first / last row</dd> | |
| 142 | 180 | </div> | |
| 143 | 181 | <div> | |
| 144 | 182 | <dt> | |
| 145 | - | <kbd>g c</kbd> | |
| 146 | - | <kbd>g r</kbd> | |
| 183 | + | <kbd>/</kbd> | |
| 147 | 184 | </dt> | |
| 148 | - | <dd>go commits / refs</dd> | |
| 185 | + | <dd>filter the list</dd> | |
| 149 | 186 | </div> | |
| 150 | 187 | <div> | |
| 151 | 188 | <dt> | |
| 152 | 189 | <kbd>b</kbd> | |
| 153 | 190 | </dt> | |
| 154 | - | <dd>switch ref (branches, tags)</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> | |
| 191 | + | <dd>switch branch or tag</dd> | |
| 168 | 192 | </div> | |
| 169 | 193 | <div> | |
| 170 | 194 | <dt> | |
| @@ -172,15 +196,21 @@ | |||
| 172 | 196 | <kbd>t</kbd> | |
| 173 | 197 | <kbd>c</kbd> | |
| 174 | 198 | <kbd>r</kbd> | |
| 199 | + | <kbd>m</kbd> | |
| 175 | 200 | </dt> | |
| 176 | - | <dd>repo sections (when shown)</dd> | |
| 201 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 177 | 202 | </div> | |
| 178 | 203 | <div> | |
| 179 | 204 | <dt> | |
| 180 | - | <kbd>m</kbd> | |
| 181 | - | <kbd>g m</kbd> | |
| 205 | + | <kbd>g h</kbd> | |
| 182 | 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 | 214 | </div> | |
| 185 | 215 | <div> | |
| 186 | 216 | <dt> | |
| @@ -200,19 +230,21 @@ | |||
| 200 | 230 | <dt> | |
| 201 | 231 | <kbd>esc</kbd> | |
| 202 | 232 | </dt> | |
| 203 | - | <dd>close panel</dd> | |
| 233 | + | <dd>close a dialog, leave a field</dd> | |
| 204 | 234 | </div> | |
| 205 | 235 | </dl> | |
| 206 | 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 | 238 | <div class="panel-head"> | |
| 209 | - | <label for="refs-filter">ref></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))"> | |
| 211 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 239 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 241 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 242 | + | <kbd>esc</kbd> | |
| 243 | + | </button> | |
| 212 | 244 | </div> | |
| 213 | 245 | <div class="panel-cols" data-picklist> | |
| 214 | 246 | <section aria-label="Branches"> | |
| 215 | - | <h2>branches <span class="mute">2</span> | |
| 247 | + | <h2>Branches <span class="count">2</span> | |
| 216 | 248 | </h2> | |
| 217 | 249 | <ul> | |
| 218 | 250 | <li> | |
| @@ -226,7 +258,7 @@ | |||
| 226 | 258 | </ul> | |
| 227 | 259 | </section> | |
| 228 | 260 | <section aria-label="Tags"> | |
| 229 | - | <h2>tags <span class="mute">1</span> | |
| 261 | + | <h2>Tags <span class="count">1</span> | |
| 230 | 262 | </h2> | |
| 231 | 263 | <ul> | |
| 232 | 264 | <li> | |
| @@ -237,110 +269,130 @@ | |||
| 237 | 269 | </section> | |
| 238 | 270 | </div> | |
| 239 | 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 | 273 | <div class="panel-head"> | |
| 242 | - | <label for="cmd-q" class="hi">:</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(); | |
| 274 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 276 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 245 | 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 | 278 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 247 | 279 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 248 | 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 | 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 | 285 | </div> | |
| 252 | - | <ul id="cmd-list" class="cmd-list"> | |
| 286 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 253 | 287 | <li data-k="go home" data-cursor> | |
| 254 | 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 | 292 | </li> | |
| 257 | 293 | <li data-k="section summary"> | |
| 258 | 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 | 298 | </li> | |
| 261 | - | <li data-k="section tree"> | |
| 299 | + | <li data-k="section files"> | |
| 262 | 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 | 304 | </li> | |
| 265 | 305 | <li data-k="section commits"> | |
| 266 | 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 | 310 | </li> | |
| 269 | - | <li data-k="section refs"> | |
| 311 | + | <li data-k="section branches & tags"> | |
| 270 | 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 | 316 | </li> | |
| 273 | 317 | <li data-k="section merge requests"> | |
| 274 | 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 | 322 | </li> | |
| 277 | 323 | <li data-k="branch feature/x"> | |
| 278 | 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 | 328 | </li> | |
| 281 | 329 | <li data-k="branch main"> | |
| 282 | 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 | 334 | </li> | |
| 285 | 335 | <li data-k="tag v1.0"> | |
| 286 | 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 | 340 | </li> | |
| 289 | 341 | <li data-k="file x/y.txt"> | |
| 290 | 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 | 346 | </li> | |
| 293 | 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 | 357 | </dialog> | |
| 295 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 358 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 311 | 359 | if (q('dialog[open]').count) return; | |
| 312 | 360 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 313 | 361 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 314 | 362 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 315 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 316 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 363 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 367 | })(1); | |
| 318 | - | k-k from:body -> ((d) => { | |
| 368 | + | k-k from:body -> (async (d) => { | |
| 319 | 369 | if (q('dialog[open]').count) return; | |
| 320 | 370 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 321 | 371 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 322 | 372 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 323 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 324 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 373 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 377 | })(-1); | |
| 326 | 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 | 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 | 380 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 329 | 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(); | |
| 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()); | |
| 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(); | |
| 382 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 385 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 334 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 335 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 336 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 337 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 338 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 339 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 340 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 341 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 342 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 343 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 386 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 387 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 388 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 389 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 390 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 391 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 392 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 393 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 394 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 395 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 344 | 396 | k-next from:body -> { | |
| 345 | 397 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 346 | 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 | 6 | <meta name="color-scheme" content="light dark"> | |
| 7 | 7 | <title>docs · files · example.test</title> | |
| 8 | 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 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 10 | 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 11 | 12 | </script> | |
| @@ -16,12 +17,15 @@ | |||
| 16 | 17 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 17 | 18 | </script> | |
| 18 | 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 | 21 | <a class="skip" href="#main">skip to content</a> | |
| 21 | - | <header class="bar top"> | |
| 22 | - | <a class="blk" href="/" data-g="h" title="home (g h)">example.test</a> | |
| 23 | - | <span class="fade" aria-hidden="true"> | |
| 22 | + | <header class="hdr"> | |
| 23 | + | <div class="hdr-top"> | |
| 24 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 25 | + | <span class="brand-mark" aria-hidden="true"> | |
| 24 | 26 | </span> | |
| 27 | + | <span>example.test</span> | |
| 28 | + | </a> | |
| 25 | 29 | <nav class="crumbs" aria-label="Path"> | |
| 26 | 30 | <ol> | |
| 27 | 31 | <li> | |
| @@ -38,33 +42,52 @@ | |||
| 38 | 42 | </li> | |
| 39 | 43 | </ol> | |
| 40 | 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 | 62 | <ul> | |
| 43 | 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 | 66 | </li> | |
| 46 | 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 | 70 | </li> | |
| 49 | 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 | 74 | </li> | |
| 52 | 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 | 78 | </li> | |
| 55 | 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 | 82 | </li> | |
| 58 | 83 | </ul> | |
| 59 | 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 | 85 | </header> | |
| 63 | 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 | 88 | <div class="filter"> | |
| 66 | - | <label for="tree-filter">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])')"> | |
| 89 | + | <label for="tree-filter" class="sr">filter</label> | |
| 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 | 91 | </div> | |
| 69 | 92 | <table class="list tree" aria-label="Entries"> | |
| 70 | 93 | <thead> | |
| @@ -115,79 +138,82 @@ | |||
| 115 | 138 | </tr> | |
| 116 | 139 | </tbody> | |
| 117 | 140 | </table> | |
| 118 | - | <h2 class="rule-h"> | |
| 119 | - | <span>README.md</span> | |
| 120 | - | </h2> | |
| 141 | + | <section class="card readme" aria-labelledby="readme-h"> | |
| 142 | + | <h2 class="card-h" id="readme-h"> | |
| 143 | + | <span class="ico i-file" aria-hidden="true"> | |
| 144 | + | </span> README.md</h2> | |
| 121 | 145 | <article class="prose" aria-label="README.md"> | |
| 122 | 146 | <h1 id="md-docs">Docs</h1> | |
| 123 | 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 | 148 | </article> | |
| 149 | + | </section> | |
| 125 | 150 | </main> | |
| 126 | 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 | 167 | <div class="panel-head"> | |
| 129 | - | <span>keys</span> | |
| 130 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 168 | + | <h2>Keyboard shortcuts</h2> | |
| 169 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 170 | + | <kbd>esc</kbd> close</button> | |
| 131 | 171 | </div> | |
| 132 | 172 | <dl class="keys"> | |
| 133 | 173 | <div> | |
| 134 | 174 | <dt> | |
| 135 | - | <kbd>j</kbd> | |
| 136 | - | <kbd>k</kbd> | |
| 175 | + | <kbd>ctrl k</kbd> | |
| 176 | + | <kbd>:</kbd> | |
| 137 | 177 | </dt> | |
| 138 | - | <dd>move cursor down / up</dd> | |
| 178 | + | <dd>go to a section, branch, tag, file or repository</dd> | |
| 139 | 179 | </div> | |
| 140 | 180 | <div> | |
| 141 | 181 | <dt> | |
| 142 | - | <kbd>enter</kbd> | |
| 182 | + | <kbd>?</kbd> | |
| 143 | 183 | </dt> | |
| 144 | - | <dd>open row under cursor</dd> | |
| 184 | + | <dd>this list</dd> | |
| 145 | 185 | </div> | |
| 146 | 186 | <div> | |
| 147 | 187 | <dt> | |
| 148 | - | <kbd>g</kbd> | |
| 149 | - | <kbd>g</kbd> / <kbd>G</kbd> | |
| 188 | + | <kbd>j</kbd> | |
| 189 | + | <kbd>k</kbd> | |
| 150 | 190 | </dt> | |
| 151 | - | <dd>first / last row</dd> | |
| 191 | + | <dd>move the row cursor down / up</dd> | |
| 152 | 192 | </div> | |
| 153 | 193 | <div> | |
| 154 | 194 | <dt> | |
| 155 | - | <kbd>/</kbd> | |
| 195 | + | <kbd>enter</kbd> | |
| 156 | 196 | </dt> | |
| 157 | - | <dd>focus the filter</dd> | |
| 197 | + | <dd>open the row under the cursor</dd> | |
| 158 | 198 | </div> | |
| 159 | 199 | <div> | |
| 160 | 200 | <dt> | |
| 161 | - | <kbd>g h</kbd> | |
| 162 | - | <kbd>g t</kbd> | |
| 201 | + | <kbd>g g</kbd> | |
| 202 | + | <kbd>G</kbd> | |
| 163 | 203 | </dt> | |
| 164 | - | <dd>go home / tree</dd> | |
| 204 | + | <dd>first / last row</dd> | |
| 165 | 205 | </div> | |
| 166 | 206 | <div> | |
| 167 | 207 | <dt> | |
| 168 | - | <kbd>g c</kbd> | |
| 169 | - | <kbd>g r</kbd> | |
| 208 | + | <kbd>/</kbd> | |
| 170 | 209 | </dt> | |
| 171 | - | <dd>go commits / refs</dd> | |
| 210 | + | <dd>filter the list</dd> | |
| 172 | 211 | </div> | |
| 173 | 212 | <div> | |
| 174 | 213 | <dt> | |
| 175 | 214 | <kbd>b</kbd> | |
| 176 | 215 | </dt> | |
| 177 | - | <dd>switch ref (branches, tags)</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> | |
| 216 | + | <dd>switch branch or tag</dd> | |
| 191 | 217 | </div> | |
| 192 | 218 | <div> | |
| 193 | 219 | <dt> | |
| @@ -195,15 +221,21 @@ | |||
| 195 | 221 | <kbd>t</kbd> | |
| 196 | 222 | <kbd>c</kbd> | |
| 197 | 223 | <kbd>r</kbd> | |
| 224 | + | <kbd>m</kbd> | |
| 198 | 225 | </dt> | |
| 199 | - | <dd>repo sections (when shown)</dd> | |
| 226 | + | <dd>tabs: summary, files, commits, branches & tags, merge requests</dd> | |
| 200 | 227 | </div> | |
| 201 | 228 | <div> | |
| 202 | 229 | <dt> | |
| 203 | - | <kbd>m</kbd> | |
| 204 | - | <kbd>g m</kbd> | |
| 230 | + | <kbd>g h</kbd> | |
| 205 | 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 | 239 | </div> | |
| 208 | 240 | <div> | |
| 209 | 241 | <dt> | |
| @@ -223,19 +255,21 @@ | |||
| 223 | 255 | <dt> | |
| 224 | 256 | <kbd>esc</kbd> | |
| 225 | 257 | </dt> | |
| 226 | - | <dd>close panel</dd> | |
| 258 | + | <dd>close a dialog, leave a field</dd> | |
| 227 | 259 | </div> | |
| 228 | 260 | </dl> | |
| 229 | 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 | 263 | <div class="panel-head"> | |
| 232 | - | <label for="refs-filter">ref></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))"> | |
| 234 | - | <button type="button" hx-on:click="closest('dialog').close()">[esc] close</button> | |
| 264 | + | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 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))"> | |
| 266 | + | <button type="button" class="panel-x" hx-on:click="closest('dialog').close()"> | |
| 267 | + | <kbd>esc</kbd> | |
| 268 | + | </button> | |
| 235 | 269 | </div> | |
| 236 | 270 | <div class="panel-cols" data-picklist> | |
| 237 | 271 | <section aria-label="Branches"> | |
| 238 | - | <h2>branches <span class="mute">2</span> | |
| 272 | + | <h2>Branches <span class="count">2</span> | |
| 239 | 273 | </h2> | |
| 240 | 274 | <ul> | |
| 241 | 275 | <li> | |
| @@ -249,7 +283,7 @@ | |||
| 249 | 283 | </ul> | |
| 250 | 284 | </section> | |
| 251 | 285 | <section aria-label="Tags"> | |
| 252 | - | <h2>tags <span class="mute">1</span> | |
| 286 | + | <h2>Tags <span class="count">1</span> | |
| 253 | 287 | </h2> | |
| 254 | 288 | <ul> | |
| 255 | 289 | <li> | |
| @@ -260,114 +294,136 @@ | |||
| 260 | 294 | </section> | |
| 261 | 295 | </div> | |
| 262 | 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 | 298 | <div class="panel-head"> | |
| 265 | - | <label for="cmd-q" class="hi">:</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(); | |
| 299 | + | <label for="cmd-q" class="sr">go to</label> | |
| 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 | 301 | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 268 | 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 | 303 | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 270 | 304 | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 271 | 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 | 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 | 310 | </div> | |
| 275 | - | <ul id="cmd-list" class="cmd-list"> | |
| 311 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 276 | 312 | <li data-k="go home" data-cursor> | |
| 277 | 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 | 317 | </li> | |
| 280 | 318 | <li data-k="section summary"> | |
| 281 | 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 | 323 | </li> | |
| 284 | - | <li data-k="section tree"> | |
| 324 | + | <li data-k="section files"> | |
| 285 | 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 | 329 | </li> | |
| 288 | 330 | <li data-k="section commits"> | |
| 289 | 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 | 335 | </li> | |
| 292 | - | <li data-k="section refs"> | |
| 336 | + | <li data-k="section branches & tags"> | |
| 293 | 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 | 341 | </li> | |
| 296 | 342 | <li data-k="section merge requests"> | |
| 297 | 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 | 347 | </li> | |
| 300 | 348 | <li data-k="branch feature/x"> | |
| 301 | 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 | 353 | </li> | |
| 304 | 354 | <li data-k="branch main"> | |
| 305 | 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 | 359 | </li> | |
| 308 | 360 | <li data-k="tag v1.0"> | |
| 309 | 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 | 365 | </li> | |
| 312 | 366 | <li data-k="file docs/readme.md"> | |
| 313 | 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 | 371 | </li> | |
| 316 | 372 | <li data-k="file docs/guide.md"> | |
| 317 | 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 | 377 | </li> | |
| 320 | 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 | 388 | </dialog> | |
| 322 | - | <footer class="bar status" aria-label="Status"> | |
| 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) => { | |
| 389 | + | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 338 | 390 | if (q('dialog[open]').count) return; | |
| 339 | 391 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 340 | 392 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 341 | 393 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 342 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 343 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 394 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 398 | })(1); | |
| 345 | - | k-k from:body -> ((d) => { | |
| 399 | + | k-k from:body -> (async (d) => { | |
| 346 | 400 | if (q('dialog[open]').count) return; | |
| 347 | 401 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 348 | 402 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 349 | 403 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 350 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 351 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 404 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 408 | })(-1); | |
| 353 | 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 | 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 | 411 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 356 | 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(); | |
| 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()); | |
| 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(); | |
| 413 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 416 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 361 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 362 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 363 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 364 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 365 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 366 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 367 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 368 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 369 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 370 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 417 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 418 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 419 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 420 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 421 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 422 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 423 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 424 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 425 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 426 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 371 | 427 | k-next from:body -> { | |
| 372 | 428 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 373 | 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 | 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 | 127 | // unescape undoes the HTML escaping of an attribute value. | |
| 128 | 128 | func unescape(s string) string { return strings.ReplaceAll(s, "&", "&") } | |
| @@ -174,12 +174,12 @@ func TestWebCommits(t *testing.T) { | |||
| 174 | 174 | assert.Equal(t, 50, strings.Count(first, `class="c-sha mute"`), "a page lists 50 commits") | |
| 175 | 175 | assert.Contains(t, first, "commit number 51") | |
| 176 | 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 | 178 | second := unescape(reOlder.FindStringSubmatch(first)[1]) | |
| 179 | 179 | page2 := get(second, 200) | |
| 180 | 180 | assert.Equal(t, 2, strings.Count(page2, `class="c-sha mute"`)) | |
| 181 | - | assert.Contains(t, page2, `href="/long/commits/main">← newest</a>`) | |
| 182 | - | assert.Contains(t, page2, "<span class=\"mute\">older →</span>") | |
| 181 | + | assert.Contains(t, page2, `href="/long/commits/main">← Newest</a>`) | |
| 182 | + | assert.Contains(t, page2, `<span class="btn" aria-disabled="true">Older →</span>`) | |
| 183 | 183 | get("/long/commits/main?page=garbage", 404) | |
| 184 | 184 | }) | |
| 185 | 185 | ||
| @@ -196,8 +196,8 @@ func TestWebCommits(t *testing.T) { | |||
| 196 | 196 | assert.Contains(t, get("/diffs/commits/v1.0.0", 200), "modify a file") | |
| 197 | 197 | assert.Contains(t, get("/diffs/commits/side", 200), "work on the side") | |
| 198 | 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]`) | |
| 200 | - | assert.Contains(t, get("/diffs/commits/main", 200), `(merge)`) | |
| 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), `<span class="pill">merge</span>`) | |
| 201 | 201 | get("/diffs/commits/nope", 404) | |
| 202 | 202 | get("/diffs/commits/mai", 404) | |
| 203 | 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 | 141 | assert.Contains(t, mr.body, "Finds things.\nSee ", "line breaks are kept") | |
| 142 | 142 | assert.NotContains(t, mr.body, "<form", "an anonymous visitor gets no form") | |
| 143 | 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 | 145 | assert.Contains(t, mr.body, `href="/proj/commit/`+head+`"`, "the commits are in the rail") | |
| 146 | 146 | assert.NotContains(t, strings.ToLower(mr.body), "proposal") | |
| 147 | 147 | assertNoInline(t, mr.body) | |
| @@ -303,7 +303,7 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||
| 303 | 303 | assert.Equal(t, base+"#c1", res.hdr.Get("Location")) | |
| 304 | 304 | own = b.get(base, as(token)) | |
| 305 | 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 | 307 | assert.Equal(t, http.StatusSeeOther, b.form(base+"/comments/1/resolve", token, url.Values{"resolved": {"false"}}).status) | |
| 308 | 308 | assert.Equal(t, http.StatusBadRequest, b.form(base+"/comments/1/resolve", token, url.Values{"resolved": {"maybe"}}).status) | |
| 309 | 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 | 312 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/read"`) | |
| 313 | 313 | res = b.form(base+"/read", token, url.Values{"head_id": {head1}}) | |
| 314 | 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 | 316 | r.writeFile(r.wt, "extra.txt", []byte("extra\n")) | |
| 317 | 317 | r.mustGit(r.wt, "push", "-q", r.url, "feat/search") | |
| 318 | 318 | head2 := r.headOf("feat/search") | |
| @@ -321,7 +321,7 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||
| 321 | 321 | assert.Contains(t, all.body, `href="/proj/merge-requests/1/since"`) | |
| 322 | 322 | assert.Contains(t, all.body, "since your last read") | |
| 323 | 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 | 325 | assert.Contains(t, all.body, "outdated", "the notes were written on the earlier head") | |
| 326 | 326 | since := b.get(base+"/since", as(token)) | |
| 327 | 327 | require.Equal(t, 200, since.status, since.body) | |
| @@ -356,14 +356,14 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||
| 356 | 356 | assert.Contains(t, done.body, "fast-forward") | |
| 357 | 357 | assert.Contains(t, done.body, `href="/proj/commit/`+head2+`"`) | |
| 358 | 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 | 360 | // Writes on a merged request are refused with a page. | |
| 361 | 361 | late := b.form(base+"/comments", token, url.Values{"text": {"too late"}}) | |
| 362 | 362 | assert.Equal(t, http.StatusConflict, late.status) | |
| 363 | 363 | assert.Equal(t, http.StatusConflict, b.form(base+"/close", token, nil).status) | |
| 364 | 364 | list := b.get("/proj/merge-requests", nil) | |
| 365 | - | assert.Contains(t, list.body, "merged and closed") | |
| 366 | - | assert.Regexp(t, `(?s)merged and closed.*Add search`, list.body) | |
| 365 | + | assert.Contains(t, list.body, "Merged and closed") | |
| 366 | + | assert.Regexp(t, `(?s)Merged and closed.*Add search`, list.body) | |
| 367 | 367 | } | |
| 368 | 368 | ||
| 369 | 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 | 418 | require.True(t, third >= 0 && squash >= 0 && other >= 0, list) | |
| 419 | 419 | assert.Less(t, third, squash, "the open request comes first") | |
| 420 | 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 | 422 | assert.Less(t, other, squash, "newest first among the decided") | |
| 423 | 423 | ||
| 424 | 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 | 116 | assert.NotContains(t, home.body, "secret") | |
| 117 | 117 | assert.Equal(t, "no-cache", home.hdr.Get("Cache-Control")) | |
| 118 | 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 | 120 | assert.Equal(t, 404, b.get("/secret", nil).status) | |
| 121 | 121 | ||
| 122 | 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 | 199 | home = b.get("/", as(token)) | |
| 200 | 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 | 202 | assert.Contains(t, b.get("/secret/blob/main/notes.txt", as(token)).body, "notes.txt") | |
| 203 | 203 | raw := b.get("/secret/raw/main/notes.txt", as(token)) | |
| 204 | 204 | assert.Equal(t, 200, raw.status) | |
M cmd/ermite/web_test.go +19 -2
| @@ -184,6 +184,7 @@ var ( | |||
| 184 | 184 | reStyleAttr = regexp.MustCompile(`(?i)\sstyle\s*=`) | |
| 185 | 185 | reStyleTag = regexp.MustCompile(`(?i)<style\b`) | |
| 186 | 186 | reImport = regexp.MustCompile(`(?i)@import|url\(`) | |
| 187 | + | reCSSURL = regexp.MustCompile(`url\(\s*([^)]*?)\s*\)`) | |
| 187 | 188 | ) | |
| 188 | 189 | ||
| 189 | 190 | // assertNoInline checks the page has no inline script, style element or | |
| @@ -231,14 +232,30 @@ func TestWebContentSecurity(t *testing.T) { | |||
| 231 | 232 | assert.Equal(t, "public, max-age=31536000, immutable", ah.Get("Cache-Control"), m[1]) | |
| 232 | 233 | assert.Equal(t, "nosniff", ah.Get("X-Content-Type-Options"), m[1]) | |
| 233 | 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 | 247 | // The footer links to the source (AGPL §13). | |
| 239 | - | _, _, body := h.get("/") | |
| 248 | + | _, hdr, body := h.get("/") | |
| 240 | 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 | 259 | // Unknown, stale or traversing asset names are 404s. | |
| 243 | 260 | for _, p := range []string{"/-/static/css/ermite.000000000000.css", "/-/static/css/ermite.css", "/-/static/", "/-/static/../x", "/-/static/js/"} { | |
| 244 | 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 | 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | |
| 11 | 11 | | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | | | |
| 12 | 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 | 19 | The three htmx files are the exact files the approved mockup was built with. | |
| 16 | 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 | 23 | // the content, so they can be cached for ever. | |
| 24 | 24 | type Assets struct { | |
| 25 | 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 | 32 | // asset is one embedded file under its content-hashed name. | |
| @@ -49,8 +53,16 @@ var scripts = []struct { | |||
| 49 | 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 | 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 | 66 | func loadAssets() (*assetSet, error) { | |
| 55 | 67 | s := &assetSet{byName: map[string]asset{}} | |
| 56 | 68 | put := func(dir, file string, body []byte, typ string) string { | |
| @@ -61,6 +73,26 @@ func loadAssets() (*assetSet, error) { | |||
| 61 | 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 | 96 | files, err := fs.Glob(staticFS, "static/css/*.css") | |
| 65 | 97 | if err != nil { | |
| 66 | 98 | return nil, err | |
| @@ -75,7 +107,14 @@ func loadAssets() (*assetSet, error) { | |||
| 75 | 107 | css.WriteString("/* " + path.Base(f) + " */\n") | |
| 76 | 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 | 119 | for _, sc := range scripts { | |
| 81 | 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 | 188 | pinRef(&l, t) | |
| 189 | 189 | l.Crumbs = pathCrumbs(d.name(), "blob", t) | |
| 190 | 190 | l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance | |
| 191 | - | l.Status = Status{ | |
| 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" | |
| 191 | + | l.HasRail = true | |
| 196 | 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 | 12 | @Shell(l, blobRail(p)) { | |
| 13 | 13 | <h1 class="file-h"> | |
| 14 | 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 | 16 | </h1> | |
| 17 | 17 | if p.Notice != "" { | |
| 18 | 18 | <p class="notice" role="note">{ p.Notice }</p> | |
| @@ -39,7 +39,7 @@ templ Blob(l Layout, p BlobPage) { | |||
| 39 | 39 | // CodeView is the highlighted lines with a link on each number. A click on a | |
| 40 | 40 | // number selects the line, shift-click extends the selection to a range; the | |
| 41 | 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 | 43 | templ CodeView(h template.HTML) { | |
| 44 | 44 | <pre | |
| 45 | 45 | class="chroma code" | |
| @@ -59,12 +59,12 @@ templ CodeView(h template.HTML) { | |||
| 59 | 59 | ||
| 60 | 60 | templ blobRail(p BlobPage) { | |
| 61 | 61 | <section aria-labelledby="r-file"> | |
| 62 | - | <h2 id="r-file">file</h2> | |
| 62 | + | <h2 id="r-file">File</h2> | |
| 63 | 63 | @Facts(p.Facts) | |
| 64 | 64 | </section> | |
| 65 | 65 | if p.Last != nil { | |
| 66 | 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 | 68 | <p><a href={ templ.SafeURL(p.Last.CHref) }>{ p.Last.Short }</a> { p.Last.Subject }</p> | |
| 69 | 69 | <p class="dim"> | |
| 70 | 70 | { p.Last.Author } · | |
| @@ -76,13 +76,13 @@ templ blobRail(p BlobPage) { | |||
| 76 | 76 | } | |
| 77 | 77 | if len(p.Outline) > 0 { | |
| 78 | 78 | <section aria-labelledby="r-outline"> | |
| 79 | - | <h2 id="r-outline">outline</h2> | |
| 79 | + | <h2 id="r-outline">On this page</h2> | |
| 80 | 80 | @Outline(p.Outline) | |
| 81 | 81 | </section> | |
| 82 | 82 | } | |
| 83 | 83 | if p.HasCode { | |
| 84 | 84 | <section aria-labelledby="r-sel"> | |
| 85 | - | <h2 id="r-sel">selection</h2> | |
| 85 | + | <h2 id="r-sel">Selection</h2> | |
| 86 | 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 | 87 | <p class="dim">y copies a permalink pinned to the commit sha</p> | |
| 88 | 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 | 157 | {Label: "commit", Href: history}, | |
| 158 | 158 | {Label: p.Short, Current: true}, | |
| 159 | 159 | } | |
| 160 | - | l.Status = Status{ | |
| 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") | |
| 160 | + | l.HasRail = true | |
| 165 | 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 | 164 | // person converts a signature. | |
| 174 | 165 | func person(s *pb.Signature, now time.Time) Person { | |
| 175 | 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 | 94 | } else { | |
| 95 | 95 | l.Title = "history of " + visibleText(sel.path) + " · " + d.name() + " · " + s.instance | |
| 96 | 96 | } | |
| 97 | - | what := plural(len(cs), "commit", "commits") | |
| 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" | |
| 97 | + | l.HasRail = true | |
| 109 | 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 | 7 | templ Commits(l Layout, p CommitsPage) { | |
| 8 | 8 | @Shell(l, commitsRail(p)) { | |
| 9 | 9 | <div class="filter"> | |
| 10 | - | <label for="commit-filter">filter></label> | |
| 10 | + | <label for="commit-filter" class="sr">filter</label> | |
| 11 | 11 | <input | |
| 12 | 12 | id="commit-filter" | |
| 13 | 13 | type="search" | |
| 14 | 14 | data-filter | |
| 15 | - | placeholder="subject, sha or author ( / )" | |
| 15 | + | placeholder="Filter by subject, sha or author…" | |
| 16 | 16 | autocomplete="off" | |
| 17 | 17 | hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | |
| 18 | 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 | 39 | if p.Older != "" || p.Newest != "" { | |
| 40 | 40 | <nav class="pager" aria-label="Pages"> | |
| 41 | 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 | 43 | } else { | |
| 44 | - | <span class="mute">← newest</span> | |
| 44 | + | <span class="btn" aria-disabled="true">← Newest</span> | |
| 45 | 45 | } | |
| 46 | 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 | 48 | } else { | |
| 49 | - | <span class="mute">older →</span> | |
| 49 | + | <span class="btn" aria-disabled="true">Older →</span> | |
| 50 | 50 | } | |
| 51 | 51 | </nav> | |
| 52 | 52 | } | |
| @@ -55,7 +55,7 @@ templ Commits(l Layout, p CommitsPage) { | |||
| 55 | 55 | ||
| 56 | 56 | templ commitsRail(p CommitsPage) { | |
| 57 | 57 | <section aria-labelledby="r-hist"> | |
| 58 | - | <h2 id="r-hist">history</h2> | |
| 58 | + | <h2 id="r-hist">History</h2> | |
| 59 | 59 | <dl class="kv"> | |
| 60 | 60 | <dt>ref</dt> | |
| 61 | 61 | <dd>{ p.Ref }</dd> | |
| @@ -72,7 +72,7 @@ templ commitsRail(p CommitsPage) { | |||
| 72 | 72 | </dl> | |
| 73 | 73 | </section> | |
| 74 | 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 | 76 | if len(p.Refs) > 0 { | |
| 77 | 77 | <nav class="outline" aria-label="Refs on this page"> | |
| 78 | 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 | 132 | Crumb{Label: "compare", Current: false}, | |
| 133 | 133 | Crumb{Label: p.Source, Current: true}, | |
| 134 | 134 | ) | |
| 135 | - | l.Status = Status{ | |
| 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" | |
| 135 | + | l.HasRail = true | |
| 140 | 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 | 21 | <span class="mute" aria-hidden="true">$ </span><code>{ line }</code> | |
| 22 | 22 | <button | |
| 23 | 23 | type="button" | |
| 24 | - | class="copy" | |
| 25 | - | data-label="[copy]" | |
| 24 | + | class="btn copy" | |
| 25 | + | data-label="Copy" | |
| 26 | 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" | |
| 28 | - | >[copy]</button> | |
| 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> | |
| 29 | 29 | </div> | |
| 30 | 30 | } | |
| 31 | 31 | </div> | |
| @@ -38,7 +38,7 @@ templ HomeRowView(r HomeRow) { | |||
| 38 | 38 | <td class="c-name"> | |
| 39 | 39 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | |
| 40 | 40 | if r.Fresh { | |
| 41 | - | <span class="tagged">● new push</span> | |
| 41 | + | <span class="pill new">new push</span> | |
| 42 | 42 | } | |
| 43 | 43 | </td> | |
| 44 | 44 | <td class="c-desc dim">{ r.Desc }</td> | |
| @@ -62,6 +62,17 @@ templ Outline(headings []Heading) { | |||
| 62 | 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 | 76 | // ReadmeView is a rendered README. Its HTML comes from the Markdown | |
| 66 | 77 | // renderer, which drops raw HTML and filters URLs. | |
| 67 | 78 | templ ReadmeView(r Readme) { | |
| @@ -91,13 +102,13 @@ templ CommitRows(rows []CommitRow) { | |||
| 91 | 102 | <td class="c-subject"> | |
| 92 | 103 | <a href={ templ.SafeURL(c.CHref) }>{ c.Subject }</a> | |
| 93 | 104 | if c.Merge { | |
| 94 | - | <span class="tagged">(merge)</span> | |
| 105 | + | <span class="pill">merge</span> | |
| 95 | 106 | } | |
| 96 | 107 | for _, b := range c.Branches { | |
| 97 | - | <span class="tagged">[{ b }]</span> | |
| 108 | + | <span class="pill">{ b }</span> | |
| 98 | 109 | } | |
| 99 | 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 | 113 | </td> | |
| 103 | 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 | 44 | <dt>refs</dt> | |
| 45 | 45 | <dd> | |
| 46 | 46 | for _, r := range p.Refs { | |
| 47 | - | [{ r }]{ " " } | |
| 47 | + | <span class="pill">{ r }</span> | |
| 48 | 48 | } | |
| 49 | 49 | </dd> | |
| 50 | 50 | } | |
| @@ -53,9 +53,9 @@ templ Commit(l Layout, p CommitPage) { | |||
| 53 | 53 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | |
| 54 | 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 | 55 | <span class="grow"></span> | |
| 56 | - | @viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref) | |
| 57 | - | @viewLink(p.DiffView, "split", "[d] 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> | |
| 56 | + | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) | |
| 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'))" class="btn">Collapse all<kbd>x</kbd></button> | |
| 59 | 59 | </div> | |
| 60 | 60 | if p.Invisible { | |
| 61 | 61 | <p class="notice"> | |
| @@ -99,7 +99,7 @@ templ Commit(l Layout, p CommitPage) { | |||
| 99 | 99 | templ viewLink(p DiffView, view, label, key, href string) { | |
| 100 | 100 | <a | |
| 101 | 101 | href={ templ.SafeURL(href) } | |
| 102 | - | class="vbtn" | |
| 102 | + | class="btn vbtn" | |
| 103 | 103 | data-view={ view } | |
| 104 | 104 | aria-keyshortcuts={ key } | |
| 105 | 105 | if p.View == view { | |
| @@ -109,7 +109,7 @@ templ viewLink(p DiffView, view, label, key, href string) { | |||
| 109 | 109 | hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" | |
| 110 | 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 | 115 | templ personView(pr Person) { | |
| @@ -121,7 +121,7 @@ templ personView(pr Person) { | |||
| 121 | 121 | ||
| 122 | 122 | templ commitRail(p CommitPage) { | |
| 123 | 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 | 125 | @FileList(p.Files) | |
| 126 | 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 | 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 | 23 | // EventService.Watch as the visitor of the request and turns each event into | |
| 24 | 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 | 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 | 13 | // ago renders a short relative date: now, 5m ago, 3h ago, 12d ago, 5mo ago, | |
| 22 | 14 | // 2y ago. | |
| 23 | 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 | 76 | </div> | |
| 77 | 77 | } | |
| 78 | 78 | ||
| 79 | - | // Live is the connection state of the event stream, in the status bar. | |
| 80 | - | // Without a stream it reads "offline" and does nothing. The events of the | |
| 81 | - | // stream are fragments that ermiteLive (ermite.js) swaps in the page. | |
| 79 | + | // Live is the connection to the event stream, in the hint badge. It shows | |
| 80 | + | // only once a connection was lost: a page that never connected, or that is | |
| 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 | 83 | templ Live(l Layout) { | |
| 83 | 84 | if l.Live && l.LiveURL != "" { | |
| 84 | 85 | <span | |
| 85 | - | class="st-live" | |
| 86 | - | data-state="offline" | |
| 87 | - | role="status" | |
| 86 | + | class="hint-live" | |
| 87 | + | data-state="connecting" | |
| 88 | 88 | hx-sse:connect={ l.LiveURL } | |
| 89 | 89 | hx-swap="none" | |
| 90 | - | :text="data.state" | |
| 91 | - | :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" | |
| 90 | + | :text="data.state === 'offline' ? 'live updates paused' : ''" | |
| 92 | 91 | hx-on="htmx:sse:after:connection -> data.state = 'live'; | |
| 93 | 92 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 94 | 93 | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 95 | 94 | gap -> ermiteLive.reload()" | |
| 96 | - | >offline</span> | |
| 97 | - | } else { | |
| 98 | - | <span class="st-live" data-state="offline" role="status" title="not connected to the event stream">offline</span> | |
| 95 | + | ></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 | 109 | l := s.base() | |
| 110 | 110 | l.Title = s.instance | |
| 111 | 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 | 112 | l.Cmd = append([]CmdItem{{Kind: "go", Label: "home", Href: "/"}}, repoCmds(repos, "")...) | |
| 124 | 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 | 9 | <div | |
| 10 | 10 | id="keys" | |
| 11 | 11 | hidden | |
| 12 | - | hx-on="k-j from:body -> ((d) => { | |
| 12 | + | hx-on="k-j from:body -> (async (d) => { | |
| 13 | 13 | if (q('dialog[open]').count) return; | |
| 14 | 14 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 15 | 15 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 16 | 16 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 17 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 18 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 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' }); | |
| 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 | 21 | })(1); | |
| 20 | - | k-k from:body -> ((d) => { | |
| 22 | + | k-k from:body -> (async (d) => { | |
| 21 | 23 | if (q('dialog[open]').count) return; | |
| 22 | 24 | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 23 | 25 | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 24 | 26 | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 25 | - | let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))]; | |
| 26 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }) | |
| 27 | + | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 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 | 31 | })(-1); | |
| 28 | 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 | 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 | 34 | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 31 | 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(); | |
| 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()); | |
| 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(); | |
| 36 | + | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 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()); | |
| 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 | 39 | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 36 | - | k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click(); | |
| 37 | - | k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 38 | - | k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 39 | - | k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 40 | - | k-s from:body -> document.querySelector('.secs [data-key=s]')?.click(); | |
| 41 | - | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 42 | - | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 43 | - | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 44 | - | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 45 | - | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 40 | + | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 41 | + | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 42 | + | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 43 | + | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 44 | + | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 45 | + | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 46 | + | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 47 | + | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 48 | + | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 49 | + | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 46 | 50 | k-next from:body -> { | |
| 47 | 51 | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 48 | 52 | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
M internal/adapters/web/keys_templ.go +1 -1
| @@ -34,7 +34,7 @@ func Keys() templ.Component { | |||
| 34 | 34 | templ_7745c5c3_Var1 = templ.NopComponent | |
| 35 | 35 | } | |
| 36 | 36 | ctx = templ.ClearChildren(ctx) | |
| 37 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"keys\" hidden hx-on=\"k-j from:body -> ((d) => {\n if (q('dialog[open]').count) return;\n let rows = q('[data-rows] > :not([hidden])').arr();\n if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180));\n let i = rows.findIndex(r => r.hasAttribute('data-cursor'));\n let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))];\n q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' })\n })(1);\n k-k from:body -> ((d) => {\n if (q('dialog[open]').count) return;\n let rows = q('[data-rows] > :not([hidden])').arr();\n if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180));\n let i = rows.findIndex(r => r.hasAttribute('data-cursor'));\n let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))];\n q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' })\n })(-1);\n k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0));\n 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));\n k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click();\n k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close());\n k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show();\n 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());\n k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus();\n k-filter from:body -> q('[data-filter]').arr()[0]?.focus();\n k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click();\n k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click();\n k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click();\n k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click();\n k-s from:body -> document.querySelector('.secs [data-key=s]')?.click();\n k-t from:body -> document.querySelector('.secs [data-key=t]')?.click();\n k-c from:body -> document.querySelector('.secs [data-key=c]')?.click();\n k-r from:body -> document.querySelector('.secs [data-key=r]')?.click();\n k-m from:body -> document.querySelector('.secs [data-key=m]')?.click();\n k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click();\n k-next from:body -> {\n let ns = q('#diffs .cmt').arr(); if (!ns.length) return;\n let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3);\n let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' })\n };\n k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split');\n k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified');\n k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false);\n k-permalink from:body -> {\n let b = document.body.dataset;\n if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return }\n history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash);\n navigator.clipboard?.writeText(location.href);\n b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = ''\n }\"></div>") | |
| 37 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"keys\" hidden hx-on=\"k-j from:body -> (async (d) => {\n if (q('dialog[open]').count) return;\n let rows = q('[data-rows] > :not([hidden])').arr();\n if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180));\n let i = rows.findIndex(r => r.hasAttribute('data-cursor'));\n let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j];\n q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' });\n let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark;\n await timeout('1500ms'); if (b.nav === mark) b.nav = ''\n })(1);\n k-k from:body -> (async (d) => {\n if (q('dialog[open]').count) return;\n let rows = q('[data-rows] > :not([hidden])').arr();\n if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180));\n let i = rows.findIndex(r => r.hasAttribute('data-cursor'));\n let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j];\n q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' });\n let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark;\n await timeout('1500ms'); if (b.nav === mark) b.nav = ''\n })(-1);\n k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0));\n 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));\n k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click();\n k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close());\n k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal();\n 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());\n k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus();\n k-filter from:body -> q('[data-filter]').arr()[0]?.focus();\n k-gh from:body -> document.querySelector('.brand')?.click();\n k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click();\n k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click();\n k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click();\n k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click();\n k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click();\n k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click();\n k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click();\n k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click();\n k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click();\n k-next from:body -> {\n let ns = q('#diffs .cmt').arr(); if (!ns.length) return;\n let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3);\n let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' })\n };\n k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split');\n k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified');\n k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false);\n k-permalink from:body -> {\n let b = document.body.dataset;\n if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return }\n history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash);\n navigator.clipboard?.writeText(location.href);\n b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = ''\n }\"></div>") | |
| 38 | 38 | if templ_7745c5c3_Err != nil { | |
| 39 | 39 | return templ_7745c5c3_Err | |
| 40 | 40 | } | |
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 | 377 | p.Live = true | |
| 378 | 378 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mrs") | |
| 379 | 379 | } | |
| 380 | - | rows := len(p.Open) + len(p.Done) | |
| 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" | |
| 380 | + | l.HasRail = true | |
| 386 | 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 | 553 | p.Live = true | |
| 559 | 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{ | |
| 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" | |
| 555 | + | l.HasRail = true | |
| 565 | 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 | 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 | 709 | if c.GetResolved() { | |
| 719 | - | v.Resolve.Label = "[unresolve]" | |
| 710 | + | v.Resolve.Label = "Unresolve" | |
| 720 | 711 | } | |
| 721 | 712 | } | |
| 722 | 713 | switch { | |
M internal/adapters/web/mr.templ +32 -32
| @@ -12,19 +12,19 @@ templ MRList(l Layout, p MRListPage) { | |||
| 12 | 12 | <div id="mr-notice"></div> | |
| 13 | 13 | } | |
| 14 | 14 | <div class="filter"> | |
| 15 | - | <label for="mr-filter">filter></label> | |
| 15 | + | <label for="mr-filter" class="sr">filter</label> | |
| 16 | 16 | <input | |
| 17 | 17 | id="mr-filter" | |
| 18 | 18 | type="search" | |
| 19 | 19 | data-filter | |
| 20 | - | placeholder="title, branch, author or state ( / )" | |
| 20 | + | placeholder="Filter by title, branch, author or state…" | |
| 21 | 21 | autocomplete="off" | |
| 22 | 22 | hx-on:input="let t = this.value.toLowerCase(); q('.mrs tbody tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | |
| 23 | 23 | let first = q('.mrs tbody tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.mrs tbody tr')" | |
| 24 | 24 | /> | |
| 25 | 25 | </div> | |
| 26 | 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 | 28 | @mrRows(p.Open, "Open merge requests") | |
| 29 | 29 | if len(p.Open) == 0 { | |
| 30 | 30 | <p class="lede dim">no open merge requests.</p> | |
| @@ -33,7 +33,7 @@ templ MRList(l Layout, p MRListPage) { | |||
| 33 | 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 | 37 | @mrRows(p.Done, "Merged and closed merge requests") | |
| 38 | 38 | if len(p.Done) == 0 { | |
| 39 | 39 | <p class="lede dim">nothing merged or closed yet.</p> | |
| @@ -41,14 +41,14 @@ templ MRList(l Layout, p MRListPage) { | |||
| 41 | 41 | if p.Older != "" || p.Newest != "" { | |
| 42 | 42 | <nav class="pager" aria-label="Pages"> | |
| 43 | 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 | 45 | } else { | |
| 46 | - | <span class="mute">← newest</span> | |
| 46 | + | <span class="btn" aria-disabled="true">← Newest</span> | |
| 47 | 47 | } | |
| 48 | 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 | 50 | } else { | |
| 51 | - | <span class="mute">older →</span> | |
| 51 | + | <span class="btn" aria-disabled="true">Older →</span> | |
| 52 | 52 | } | |
| 53 | 53 | </nav> | |
| 54 | 54 | } | |
| @@ -80,7 +80,7 @@ templ mrRows(rows []MRRow, label string) { | |||
| 80 | 80 | ||
| 81 | 81 | templ mrListRail(p MRListPage) { | |
| 82 | 82 | <section aria-labelledby="r-states"> | |
| 83 | - | <h2 id="r-states">states</h2> | |
| 83 | + | <h2 id="r-states">States</h2> | |
| 84 | 84 | <dl class="kv"> | |
| 85 | 85 | <dt>open</dt> | |
| 86 | 86 | <dd>awaits review</dd> | |
| @@ -91,7 +91,7 @@ templ mrListRail(p MRListPage) { | |||
| 91 | 91 | </dl> | |
| 92 | 92 | </section> | |
| 93 | 93 | <section aria-labelledby="r-new"> | |
| 94 | - | <h2 id="r-new">open one</h2> | |
| 94 | + | <h2 id="r-new">Open one</h2> | |
| 95 | 95 | if len(p.Branches) > 0 { | |
| 96 | 96 | <p class="dim">compare a branch with { p.Default }:</p> | |
| 97 | 97 | <nav class="outline" aria-label="Branches"> | |
| @@ -166,9 +166,9 @@ templ MergeRequest(l Layout, p MRPage) { | |||
| 166 | 166 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | |
| 167 | 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 | 168 | <span class="grow"></span> | |
| 169 | - | @viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref) | |
| 170 | - | @viewLink(p.DiffView, "split", "[d] 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> | |
| 169 | + | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) | |
| 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'))" class="btn">Collapse all<kbd>x</kbd></button> | |
| 172 | 172 | </div> | |
| 173 | 173 | if p.Invisible { | |
| 174 | 174 | <p class="notice"> | |
| @@ -197,7 +197,7 @@ templ MergeRequest(l Layout, p MRPage) { | |||
| 197 | 197 | <p class="note">no changed files.</p> | |
| 198 | 198 | } | |
| 199 | 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 | 201 | for _, c := range p.Loose { | |
| 202 | 202 | @commentView(c, "") | |
| 203 | 203 | } | |
| @@ -257,7 +257,7 @@ templ commentView(c CommentView, suffix string) { | |||
| 257 | 257 | if c.Resolve != nil { | |
| 258 | 258 | <form class="c-act" method="post" action={ templ.SafeURL(c.Resolve.Action) }> | |
| 259 | 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 | 261 | </form> | |
| 262 | 262 | } | |
| 263 | 263 | </div> | |
| @@ -291,9 +291,9 @@ templ commentForm(f CommentForm, suffix string) { | |||
| 291 | 291 | <input type="hidden" name="side" value={ f.Side }/> | |
| 292 | 292 | } | |
| 293 | 293 | <div class="f-btns"> | |
| 294 | - | <button type="submit" class="f-main">[comment]</button> | |
| 294 | + | <button type="submit" class="btn primary">Comment</button> | |
| 295 | 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 | 298 | </div> | |
| 299 | 299 | </form> | |
| @@ -304,12 +304,12 @@ templ mrRail(p MRPage) { | |||
| 304 | 304 | @mergeBox(p) | |
| 305 | 305 | } | |
| 306 | 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 | 308 | <p class="dim">no checks</p> | |
| 309 | 309 | <p class="mute">there is no CI yet: nothing blocks the merge.</p> | |
| 310 | 310 | </section> | |
| 311 | 311 | <section aria-labelledby="r-settings"> | |
| 312 | - | <h2 id="r-settings">settings</h2> | |
| 312 | + | <h2 id="r-settings">Settings</h2> | |
| 313 | 313 | <dl class="kv"> | |
| 314 | 314 | <dt>modes</dt> | |
| 315 | 315 | <dd>{ strings.Join(p.Modes, ", ") }</dd> | |
| @@ -324,7 +324,7 @@ templ mrRail(p MRPage) { | |||
| 324 | 324 | </dl> | |
| 325 | 325 | </section> | |
| 326 | 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 | 328 | <ul class="pcommits"> | |
| 329 | 329 | for _, c := range p.Commits { | |
| 330 | 330 | <li data-new?={ c.New }> | |
| @@ -350,7 +350,7 @@ templ mrRail(p MRPage) { | |||
| 350 | 350 | } | |
| 351 | 351 | </section> | |
| 352 | 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 | 354 | @FileList(p.Files) | |
| 355 | 355 | </section> | |
| 356 | 356 | } | |
| @@ -360,7 +360,7 @@ templ mrRail(p MRPage) { | |||
| 360 | 360 | // the other things one does with the request. | |
| 361 | 361 | templ mergeBox(p MRPage) { | |
| 362 | 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 | 364 | <dl class="kv"> | |
| 365 | 365 | <dt>head</dt> | |
| 366 | 366 | <dd> | |
| @@ -386,7 +386,7 @@ templ mergeBox(p MRPage) { | |||
| 386 | 386 | <label class="d-check"><input type="checkbox" name="delete_branch" value="1"/> delete { p.Source } after the merge</label> | |
| 387 | 387 | } | |
| 388 | 388 | <div class="d-btns"> | |
| 389 | - | <button type="submit" class="d-main">[merge]</button> | |
| 389 | + | <button type="submit" class="btn primary">Merge</button> | |
| 390 | 390 | </div> | |
| 391 | 391 | </form> | |
| 392 | 392 | } | |
| @@ -394,11 +394,11 @@ templ mergeBox(p MRPage) { | |||
| 394 | 394 | if p.Box.CanMarkRead { | |
| 395 | 395 | <form method="post" action={ templ.SafeURL(p.Box.ReadAction) }> | |
| 396 | 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 | 398 | </form> | |
| 399 | 399 | } | |
| 400 | 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 | 402 | </form> | |
| 403 | 403 | </div> | |
| 404 | 404 | if !p.Box.HeadGone { | |
| @@ -438,7 +438,7 @@ templ Compare(l Layout, p ComparePage) { | |||
| 438 | 438 | <label for="mr-desc">description</label> | |
| 439 | 439 | <textarea id="mr-desc" name="description" rows="4" maxlength="8000" placeholder="plain text, optional"></textarea> | |
| 440 | 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 | 442 | </div> | |
| 443 | 443 | </form> | |
| 444 | 444 | } | |
| @@ -446,9 +446,9 @@ templ Compare(l Layout, p ComparePage) { | |||
| 446 | 446 | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | |
| 447 | 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 | 448 | <span class="grow"></span> | |
| 449 | - | @viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref) | |
| 450 | - | @viewLink(p.DiffView, "split", "[d] 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> | |
| 449 | + | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) | |
| 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'))" class="btn">Collapse all<kbd>x</kbd></button> | |
| 452 | 452 | </div> | |
| 453 | 453 | if p.Invisible { | |
| 454 | 454 | <p class="notice"> | |
| @@ -477,7 +477,7 @@ templ Compare(l Layout, p ComparePage) { | |||
| 477 | 477 | ||
| 478 | 478 | templ compareRail(p ComparePage) { | |
| 479 | 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 | 481 | <ul class="pcommits"> | |
| 482 | 482 | for _, c := range p.Ahead { | |
| 483 | 483 | <li> | |
| @@ -498,12 +498,12 @@ templ compareRail(p ComparePage) { | |||
| 498 | 498 | </section> | |
| 499 | 499 | if p.HasDiff { | |
| 500 | 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 | 502 | @FileList(p.Files) | |
| 503 | 503 | </section> | |
| 504 | 504 | } | |
| 505 | 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 | 507 | @CloneBlock(p.CLI) | |
| 508 | 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 | 109 | if back == "" { | |
| 110 | 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 | 113 | w.Header().Set("Cache-Control", "no-store") | |
| 114 | 114 | s.write(ctx, w, r, status, State(l, StatePage{ | |
| 115 | 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 | 7 | // OverlayHelp is the keyboard map panel. | |
| 8 | 8 | templ OverlayHelp() { | |
| 9 | - | <dialog id="panel-help" class="panel" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())"> | |
| 10 | - | <div class="panel-head"><span>keys</span><button type="button" hx-on:click="closest('dialog').close()">[esc] close</button></div> | |
| 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"><h2>Keyboard shortcuts</h2><button type="button" class="panel-x" hx-on:click="closest('dialog').close()"><kbd>esc</kbd> close</button></div> | |
| 11 | 11 | <dl class="keys"> | |
| 12 | - | <div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move cursor down / up</dd></div> | |
| 13 | - | <div><dt><kbd>enter</kbd></dt><dd>open row under cursor</dd></div> | |
| 14 | - | <div><dt><kbd>g</kbd> <kbd>g</kbd> / <kbd>G</kbd></dt><dd>first / last row</dd></div> | |
| 15 | - | <div><dt><kbd>/</kbd></dt><dd>focus the filter</dd></div> | |
| 16 | - | <div><dt><kbd>g h</kbd> <kbd>g t</kbd></dt><dd>go home / tree</dd></div> | |
| 17 | - | <div><dt><kbd>g c</kbd> <kbd>g r</kbd></dt><dd>go commits / refs</dd></div> | |
| 18 | - | <div><dt><kbd>b</kbd></dt><dd>{ "switch" } ref (branches, tags)</dd></div> | |
| 19 | - | <div><dt><kbd>:</kbd> <kbd>f</kbd></dt><dd>go to: section, ref, file, repo</dd></div> | |
| 20 | - | <div><dt><kbd>y</kbd></dt><dd>permalink: ref becomes the full sha</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> | |
| 22 | - | <div><dt><kbd>m</kbd> <kbd>g m</kbd></dt><dd>merge requests (when shown)</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>?</kbd></dt><dd>this list</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>enter</kbd></dt><dd>open the row under the cursor</dd></div> | |
| 16 | + | <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div> | |
| 17 | + | <div><dt><kbd>/</kbd></dt><dd>filter the list</dd></div> | |
| 18 | + | <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</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>g h</kbd></dt><dd>home</dd></div> | |
| 21 | + | <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div> | |
| 23 | 22 | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse all</dd></div> | |
| 24 | 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 | 25 | </dl> | |
| 27 | 26 | </dialog> | |
| 28 | 27 | } | |
| @@ -30,22 +29,22 @@ templ OverlayHelp() { | |||
| 30 | 29 | // OverlayRefs is the ref switcher. It exists only inside a repository. | |
| 31 | 30 | templ OverlayRefs(l Layout) { | |
| 32 | 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 | 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 | 35 | <input | |
| 37 | 36 | id="refs-filter" | |
| 38 | 37 | type="search" | |
| 39 | - | placeholder="filter branches and tags" | |
| 38 | + | placeholder="Switch branch or tag…" | |
| 40 | 39 | autocomplete="off" | |
| 41 | 40 | data-pick | |
| 42 | 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 | 44 | </div> | |
| 46 | 45 | <div class="panel-cols" data-picklist> | |
| 47 | 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 | 48 | <ul> | |
| 50 | 49 | for _, r := range l.Heads { | |
| 51 | 50 | @refItem(r) | |
| @@ -53,7 +52,7 @@ templ OverlayRefs(l Layout) { | |||
| 53 | 52 | </ul> | |
| 54 | 53 | </section> | |
| 55 | 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 | 56 | <ul> | |
| 58 | 57 | for _, r := range l.Tags { | |
| 59 | 58 | @refItem(r) | |
| @@ -87,13 +86,13 @@ templ refItem(r RefItem) { | |||
| 87 | 86 | // OverlayCmd is the ":" command line: one filterable list mixing sections, | |
| 88 | 87 | // refs, files of this directory and repos. | |
| 89 | 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 | 90 | <div class="panel-head"> | |
| 92 | - | <label for="cmd-q" class="hi">:</label> | |
| 91 | + | <label for="cmd-q" class="sr">go to</label> | |
| 93 | 92 | <input | |
| 94 | 93 | id="cmd-q" | |
| 95 | 94 | type="search" | |
| 96 | - | placeholder="section, ref, file or repo" | |
| 95 | + | placeholder="Go to a section, branch, tag, file or repository…" | |
| 97 | 96 | autocomplete="off" | |
| 98 | 97 | spellcheck="false" | |
| 99 | 98 | aria-controls="cmd-list" | |
| @@ -106,12 +105,13 @@ templ OverlayCmd(items []CmdItem) { | |||
| 106 | 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 | 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 | 109 | </div> | |
| 111 | - | <ul id="cmd-list" class="cmd-list"> | |
| 110 | + | <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets"> | |
| 112 | 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 | 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 | 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 | 6 | templ Home(l Layout, p HomePage) { | |
| 7 | 7 | @Shell(l, nil) { | |
| 8 | 8 | <div class="filter"> | |
| 9 | - | <label for="home-filter">filter></label> | |
| 9 | + | <label for="home-filter" class="sr">filter</label> | |
| 10 | 10 | <input | |
| 11 | 11 | id="home-filter" | |
| 12 | 12 | type="search" | |
| 13 | 13 | data-filter | |
| 14 | - | placeholder="name or description ( / )" | |
| 14 | + | placeholder="Filter repositories…" | |
| 15 | 15 | autocomplete="off" | |
| 16 | 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 | 52 | <p class="lede">{ p.Desc }</p> | |
| 53 | 53 | } | |
| 54 | 54 | if p.Readme != nil { | |
| 55 | - | <h2 class="rule-h"><span>{ p.Readme.Name }</span></h2> | |
| 56 | - | @ReadmeView(*p.Readme) | |
| 55 | + | @ReadmeCard(*p.Readme) | |
| 57 | 56 | } | |
| 58 | 57 | } | |
| 59 | 58 | } | |
| 60 | 59 | ||
| 61 | 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 | 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 | 64 | </section> | |
| 66 | 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 | 67 | </section> | |
| 69 | 68 | } | |
| 70 | 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 | 71 | </section> | |
| 73 | 72 | } | |
| 74 | 73 | } | |
| @@ -77,12 +76,12 @@ templ summaryRail(p SummaryPage) { | |||
| 77 | 76 | templ Refs(l Layout, p RefsPage) { | |
| 78 | 77 | @Shell(l, refsRail(p)) { | |
| 79 | 78 | <div class="filter"> | |
| 80 | - | <label for="ref-filter">filter></label> | |
| 79 | + | <label for="ref-filter" class="sr">filter</label> | |
| 81 | 80 | <input | |
| 82 | 81 | id="ref-filter" | |
| 83 | 82 | type="search" | |
| 84 | 83 | data-filter | |
| 85 | - | placeholder="branch or tag ( / )" | |
| 84 | + | placeholder="Filter branches and tags…" | |
| 86 | 85 | autocomplete="off" | |
| 87 | 86 | hx-on:input="let t = this.value.toLowerCase(); q('#ref-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | |
| 88 | 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 | 106 | <td class="c-name"> | |
| 108 | 107 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | |
| 109 | 108 | if r.Default { | |
| 110 | - | <span class="tagged">*</span> | |
| 109 | + | <span class="pill">default</span> | |
| 111 | 110 | } | |
| 112 | 111 | </td> | |
| 113 | 112 | <td class="c-sha"> | |
| @@ -128,20 +127,17 @@ templ refRow(kind string, r RefRow) { | |||
| 128 | 127 | ||
| 129 | 128 | templ refsRail(p RefsPage) { | |
| 130 | 129 | <section aria-labelledby="r-refs"> | |
| 131 | - | <h2 id="r-refs">refs</h2> | |
| 130 | + | <h2 id="r-refs">Overview</h2> | |
| 132 | 131 | @RefsKV(p) | |
| 133 | 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 | 135 | // State is a 404, 500 or empty-repository page inside the normal shell. | |
| 141 | 136 | templ State(l Layout, p StatePage) { | |
| 142 | 137 | @Shell(l, nil) { | |
| 143 | 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 | 141 | for _, line := range p.Lines { | |
| 146 | 142 | <p class="dim">{ line }</p> | |
| 147 | 143 | } | |
| @@ -162,14 +158,14 @@ templ State(l Layout, p StatePage) { | |||
| 162 | 158 | templ Login(l Layout, p LoginPage) { | |
| 163 | 159 | @Shell(l, nil) { | |
| 164 | 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 | 162 | for _, line := range p.Lines { | |
| 167 | 163 | <p class="dim">{ line }</p> | |
| 168 | 164 | } | |
| 169 | 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 | 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 | 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 | 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 | 169 | // "commits"), ref, then the segments of the path. The directories link to | |
| 170 | 170 | // their tree; the last segment links to the file for a blob, and to nothing | |
| 171 | 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 | 26 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=refs") | |
| 27 | 27 | l.Crumbs[0].Current = false | |
| 28 | 28 | l.Crumbs = append(l.Crumbs, Crumb{Label: "refs", Current: true}) | |
| 29 | - | l.Status = Status{ | |
| 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" | |
| 29 | + | l.HasRail = true | |
| 34 | 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 | 6 | "context" | |
| 7 | 7 | "slices" | |
| 8 | 8 | "sort" | |
| 9 | + | "strings" | |
| 9 | 10 | "sync" | |
| 10 | 11 | "time" | |
| 11 | 12 | ||
| @@ -98,23 +99,23 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La | |||
| 98 | 99 | l.Ref, l.RefPath = def, def | |
| 99 | 100 | l.SHA, l.Short = d.head.GetCommitId(), short(d.head.GetCommitId()) | |
| 100 | 101 | } | |
| 101 | - | mk := func(key, post, href, id string) Section { | |
| 102 | - | return Section{Key: key, Post: post, Href: href, Current: section == id} | |
| 102 | + | mk := func(key, label, href, id string) Section { | |
| 103 | + | return Section{Key: key, Label: label, Href: href, Current: section == id} | |
| 103 | 104 | } | |
| 104 | 105 | l.Sections = []Section{ | |
| 105 | - | mk("s", "ummary", repoHref(d.name()), "summary"), | |
| 106 | - | mk("t", "ree", repoHref(d.name(), "tree", def), "tree"), | |
| 107 | - | mk("c", "ommits", repoHref(d.name(), "commits", def), "commits"), | |
| 108 | - | mk("r", "efs", repoHref(d.name(), "refs"), "refs"), | |
| 106 | + | mk("s", "Summary", repoHref(d.name()), "summary"), | |
| 107 | + | mk("t", "Files", repoHref(d.name(), "tree", def), "tree"), | |
| 108 | + | mk("c", "Commits", repoHref(d.name(), "commits", def), "commits"), | |
| 109 | + | mk("r", "Branches & tags", repoHref(d.name(), "refs"), "refs"), | |
| 109 | 110 | } | |
| 110 | 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 | 114 | l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}} | |
| 114 | 115 | ||
| 115 | 116 | l.Cmd = append(l.Cmd, CmdItem{Kind: "go", Label: "home", Href: "/"}) | |
| 116 | 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 | 120 | if l.Ref != "" { | |
| 120 | 121 | for _, b := range d.branches { | |
| @@ -233,7 +234,6 @@ func (s *Server) emptyState(ctx context.Context, d *repoData) (Layout, StatePage | |||
| 233 | 234 | l.Title = d.name() + " · " + s.instance | |
| 234 | 235 | l.Sections = nil | |
| 235 | 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 | 237 | branch := d.repo.GetDefaultBranch() | |
| 238 | 238 | return l, StatePage{ | |
| 239 | 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 | 251 | s.secretPage(w, r, http.StatusNotFound, s.loginRejectedPage()) | |
| 252 | 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 | 255 | s.secretPage(w, r, http.StatusOK, Login(l, LoginPage{ | |
| 256 | 256 | Title: "sign in as the owner", | |
| 257 | 257 | Lines: []string{"this browser will be signed in for 12 hours.", "only confirm if you ran `ermite web login` just now."}, | |
| 258 | 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 | 311 | // stateOnly is a state page with a title and two lines, for the login flow. | |
| 312 | 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 | 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 | 3 | package web | |
| 4 | 4 | ||
| 5 | - | // Shell is the page: head, the two bars, the work area (content and rail), | |
| 6 | - | // the overlays and the key map. The content is the children. | |
| 5 | + | // Shell is the page: head, the header, the work area (content and rail), the | |
| 6 | + | // footer, the hint badge, the overlays and the key map. The content is the | |
| 7 | + | // children. | |
| 7 | 8 | templ Shell(l Layout, rail templ.Component) { | |
| 8 | 9 | <!DOCTYPE html> | |
| 9 | 10 | <html lang="en"> | |
| @@ -13,132 +14,143 @@ templ Shell(l Layout, rail templ.Component) { | |||
| 13 | 14 | <meta name="color-scheme" content="light dark"/> | |
| 14 | 15 | <title>{ l.Title }</title> | |
| 15 | 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 | 18 | <link rel="stylesheet" href={ templ.SafeURL(l.Assets.CSS) }/> | |
| 17 | 19 | <script src={ l.Assets.HTMX } defer></script> | |
| 18 | 20 | <script src={ l.Assets.Live } defer></script> | |
| 19 | 21 | <script src={ l.Assets.SSE } defer></script> | |
| 20 | 22 | <script src={ l.Assets.App } defer></script> | |
| 21 | 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 | 25 | <a class="skip" href="#main">skip to content</a> | |
| 24 | - | @Topbar(l) | |
| 25 | - | <div class={ "work", templ.KV("has-rail", l.HasRail), templ.KV("read", l.Read) }> | |
| 26 | - | <main id="main" tabindex="-1" data-y="0" hx-on:scroll="this.dataset.y = Math.round(this.scrollTop / 60)"> | |
| 26 | + | @Header(l) | |
| 27 | + | <div class={ "work", templ.KV("has-rail", l.HasRail && rail != nil) }> | |
| 28 | + | <main id="main" tabindex="-1"> | |
| 27 | 29 | { children... } | |
| 28 | 30 | </main> | |
| 29 | 31 | if l.HasRail && rail != nil { | |
| 30 | 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 }"> | |
| 32 | - | <summary>{ l.RailSum }</summary> | |
| 33 | - | @rail | |
| 34 | - | </details> | |
| 33 | + | @rail | |
| 35 | 34 | </aside> | |
| 36 | 35 | } | |
| 37 | 36 | </div> | |
| 37 | + | @Footer(l) | |
| 38 | + | @Hint(l) | |
| 38 | 39 | @OverlayHelp() | |
| 39 | 40 | @OverlayRefs(l) | |
| 40 | 41 | @OverlayCmd(l.Cmd) | |
| 41 | - | @Statusbar(l) | |
| 42 | 42 | @Keys() | |
| 43 | 43 | </body> | |
| 44 | 44 | </html> | |
| 45 | 45 | } | |
| 46 | 46 | ||
| 47 | - | // Topbar is the top bar, neferbar style: the one accent block (instance) | |
| 48 | - | // fading into the black bar, the path, the repo sections, the ref switcher. | |
| 49 | - | templ Topbar(l Layout) { | |
| 50 | - | <header class="bar top"> | |
| 51 | - | <a class="blk" href="/" data-g="h" title="home (g h)">{ l.Instance }</a><span class="fade" aria-hidden="true"></span> | |
| 52 | - | <nav class="crumbs" aria-label="Path"> | |
| 53 | - | <ol> | |
| 54 | - | for _, c := range l.Crumbs { | |
| 55 | - | <li> | |
| 56 | - | if c.Href != "" { | |
| 57 | - | <a | |
| 58 | - | href={ templ.SafeURL(c.Href) } | |
| 59 | - | if c.Current { | |
| 60 | - | aria-current="page" | |
| 47 | + | // Header is the page header: the instance, the path, the ref switcher, the | |
| 48 | + | // "go to" palette and the owner; inside a repository, the tabs below. | |
| 49 | + | templ Header(l Layout) { | |
| 50 | + | <header class="hdr"> | |
| 51 | + | <div class="hdr-top"> | |
| 52 | + | <a class="brand" href="/" data-g="h" title="home (g h)"> | |
| 53 | + | <span class="brand-mark" aria-hidden="true"></span> | |
| 54 | + | <span>{ l.Instance }</span> | |
| 55 | + | </a> | |
| 56 | + | if len(l.Crumbs) > 0 { | |
| 57 | + | <nav class="crumbs" aria-label="Path"> | |
| 58 | + | <ol> | |
| 59 | + | for _, c := range l.Crumbs { | |
| 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> | |
| 63 | - | } else { | |
| 64 | - | <span aria-current="page">{ c.Label }</span> | |
| 71 | + | </li> | |
| 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> | |
| 69 | - | </nav> | |
| 92 | + | <button | |
| 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 | 114 | if len(l.Sections) > 0 { | |
| 71 | - | <nav class="secs" aria-label="Sections"> | |
| 115 | + | <nav class="tabs" aria-label="Sections"> | |
| 72 | 116 | <ul> | |
| 73 | 117 | for _, s := range l.Sections { | |
| 74 | 118 | <li> | |
| 75 | 119 | <a | |
| 76 | 120 | href={ templ.SafeURL(s.Href) } | |
| 77 | 121 | data-key={ s.Key } | |
| 78 | - | aria-keyshortcuts={ "g " + s.Key } | |
| 122 | + | aria-keyshortcuts={ s.Key } | |
| 79 | 123 | if s.Current { | |
| 80 | 124 | aria-current="page" | |
| 81 | 125 | } | |
| 82 | - | >{ s.Key }{ s.Post }</a> | |
| 126 | + | >{ s.Label }<kbd aria-hidden="true">{ s.Key }</kbd></a> | |
| 83 | 127 | </li> | |
| 84 | 128 | } | |
| 85 | 129 | </ul> | |
| 86 | 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 | 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 | 135 | // Footer is the version and the link to the source of the running version, | |
| 141 | 136 | // which the AGPL §13 asks every user of the service to be offered. | |
| 142 | 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 | 1 | @layer reset, tokens, base, components, utilities; | |
| 2 | 2 | ||
| 3 | - | /* Palette: the neferbar "mono" theme, black and grays only. | |
| 4 | - | Page surfaces follow the OS theme; the two bars stay black in both themes, | |
| 5 | - | like neferbar under a light or a dark terminal. */ | |
| 3 | + | /* Fonts: Atkinson Hyperlegible Next for the interface, Atkinson Hyperlegible | |
| 4 | + | Mono for titles, code, shas, refs and keys. Both are embedded (static/fonts, | |
| 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 | 26 | @layer tokens { | |
| 7 | 27 | :root { | |
| 8 | 28 | color-scheme: light dark; | |
| 9 | 29 | ||
| 10 | - | /* page */ | |
| 11 | - | --paper: light-dark(#f2f2f2, #000000); | |
| 12 | - | --paper-alt: light-dark(#e8e8e8, #0c0c0c); | |
| 13 | - | --row: light-dark(#dddddd, #1a1a1a); | |
| 14 | - | --sel: light-dark(#cbcbcb, #303030); | |
| 15 | - | --ink: light-dark(#161616, #d0d0d0); | |
| 16 | - | --ink-hi: light-dark(#000000, #ffffff); | |
| 17 | - | --ink-dim: light-dark(#4a4a4a, #9a9a9a); | |
| 18 | - | --ink-mute: light-dark(#5c5c5c, #8a8a8a); | |
| 19 | - | --rule: light-dark(#bcbcbc, #303030); | |
| 20 | - | --focus: light-dark(#000000, #ffffff); | |
| 30 | + | /* surfaces, bottom to top */ | |
| 31 | + | --bg-canvas: light-dark(#f3f3f3, #0c0c0c); /* the page */ | |
| 32 | + | --bg-inset: light-dark(#f8f8f8, #080808); /* code, fields: sunk into a surface */ | |
| 33 | + | --bg-surface:light-dark(#ffffff, #141414); /* cards, header, rail blocks */ | |
| 34 | + | --bg-raised: light-dark(#ececec, #1d1d1d); /* hover, buttons, panel rows */ | |
| 35 | + | --bg-strong: light-dark(#dedede, #292929); /* selected row, pressed button */ | |
| 36 | + | --bg-overlay:light-dark(#ffffff, #171717); /* dialogs */ | |
| 37 | + | --backdrop: light-dark(rgb(0 0 0 / .25), rgb(0 0 0 / .6)); | |
| 21 | 38 | ||
| 22 | - | /* bars (neferbar mono.conf: background, foreground, color4 accent, color0 ink) */ | |
| 23 | - | --bar: #000000; | |
| 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; | |
| 39 | + | --border: light-dark(#dddddd, #242424); | |
| 40 | + | --border-strong: light-dark(#c2c2c2, #383838); | |
| 33 | 41 | ||
| 34 | - | /* Colour, sparingly, from the owner's Despair theme: only code and diffs carry hue. | |
| 35 | - | Dark values are Despair's own; light values are the same hues darkened to AA on --paper. */ | |
| 36 | - | --teal: light-dark(#2f6b6b, #7db5b5); /* Despair bright teal: keywords, additions */ | |
| 37 | - | --teal-2: light-dark(#355f5f, #5a8585); /* slate teal: types, numbers */ | |
| 38 | - | --red: light-dark(#a8404b, #d96c75); /* soft red: deletions */ | |
| 39 | - | --rose: light-dark(#6b6570, #a7a2a9); /* rose quartz: strings */ | |
| 42 | + | --ink: light-dark(#1b1b1b, #d6d6d6); | |
| 43 | + | --ink-hi: light-dark(#000000, #fafafa); | |
| 44 | + | --ink-dim: light-dark(#4d4d4d, #a3a3a3); | |
| 45 | + | --ink-mute: light-dark(#616161, #8c8c8c); | |
| 46 | + | --focus: light-dark(#000000, #ffffff); | |
| 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 | 56 | --syn-plain: var(--ink); | |
| 42 | 57 | --syn-keyword: var(--teal); | |
| 43 | 58 | --syn-string: var(--rose); | |
| 44 | - | --syn-comment: light-dark(#5c5f63, #6a6e73); | |
| 59 | + | --syn-comment: light-dark(#5c5f63, #6e7277); | |
| 45 | 60 | --syn-number: var(--teal-2); | |
| 46 | 61 | --syn-func: var(--ink-hi); | |
| 47 | 62 | --syn-type: var(--teal-2); | |
| 48 | 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 */ | |
| 51 | - | --add: light-dark(#d8e5e5, #121919); | |
| 52 | - | --del: light-dark(#eedadc, #231113); | |
| 65 | + | --add: light-dark(#dcebeb, #111b1b); | |
| 66 | + | --del: light-dark(#f2dfe1, #231315); | |
| 53 | 67 | ||
| 54 | - | /* one family (the neferbar font when installed), three sizes, two weights */ | |
| 55 | - | --font: "Adwaita Mono", "AdwaitaMono Nerd Font Mono", ui-monospace, "Cascadia Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace; | |
| 56 | - | --fs: 14px; | |
| 68 | + | --font: "Atkinson Hyperlegible Next", system-ui, sans-serif; | |
| 69 | + | --mono: "Atkinson Hyperlegible Mono", ui-monospace, "Cascadia Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace; | |
| 70 | + | --fs: 15px; | |
| 57 | 71 | --fs-code: 13px; | |
| 58 | - | --fs-s: 12px; | |
| 59 | - | --lh: 20px; | |
| 60 | - | --lh-code: 18px; | |
| 72 | + | --fs-s: 13px; | |
| 73 | + | --fs-xs: 12px; | |
| 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; | |
| 63 | - | --bar-h: 20px; /* one text line, like neferbar: no padding above or below */ | |
| 64 | - | --stick: var(--bar-h); /* top offset of sticky headers inside the content (0 in the app shell) */ | |
| 65 | - | --measure: 80ch; | |
| 66 | - | --rail: 40ch; | |
| 89 | + | /* names used by the code, diff and merge request sheets */ | |
| 90 | + | --paper: var(--bg-canvas); | |
| 91 | + | --paper-alt: var(--bg-raised); | |
| 92 | + | --row: var(--bg-raised); | |
| 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 | 1 | @layer reset { | |
| 2 | - | *, *::before, *::after { box-sizing: border-box; border-radius: 0; } | |
| 2 | + | *, *::before, *::after { box-sizing: border-box; } | |
| 3 | 3 | html { -webkit-text-size-adjust: 100%; } | |
| 4 | 4 | body, h1, h2, h3, h4, p, ul, ol, dl, dd, figure, pre { margin: 0; } | |
| 5 | 5 | ul, ol { padding: 0; list-style: none; } | |
| 6 | 6 | table { border-collapse: collapse; border-spacing: 0; } | |
| 7 | 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 | 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 | 11 | summary { cursor: pointer; } | |
| 12 | 12 | dialog { color: inherit; border: 0; padding: 0; max-width: none; max-height: none; } | |
| 13 | 13 | img { max-width: 100%; } | |
| 14 | 14 | } | |
| 15 | 15 | ||
| 16 | 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 | 25 | body { | |
| 19 | 26 | min-height: 100dvh; | |
| 20 | - | display: grid; | |
| 21 | - | grid-template-rows: auto auto 1fr auto; | |
| 27 | + | display: flex; flex-direction: column; | |
| 22 | 28 | font: var(--fs) / var(--lh) var(--font); | |
| 23 | 29 | color: var(--ink); | |
| 24 | - | background: var(--paper); | |
| 25 | - | font-variant-ligatures: none; | |
| 30 | + | background: var(--bg-canvas); | |
| 26 | 31 | tab-size: 4; | |
| 32 | + | -webkit-font-smoothing: antialiased; | |
| 27 | 33 | } | |
| 28 | - | h1, h2, h3, h4 { font-size: inherit; font-weight: bold; } | |
| 29 | - | b, strong { font-weight: bold; } | |
| 30 | - | em, i { font-style: normal; text-decoration: underline; text-underline-offset: 3px; } | |
| 34 | + | h1, h2, h3, h4 { font-family: var(--mono); font-weight: 700; color: var(--ink-hi); letter-spacing: -.01em; } | |
| 35 | + | h1 { font-size: 22px; line-height: 30px; } | |
| 36 | + | h2 { font-size: 17px; line-height: 26px; } | |
| 37 | + | h3, h4 { font-size: var(--fs); } | |
| 38 | + | b, strong { font-weight: 700; } | |
| 31 | 39 | small { font-size: var(--fs-s); } | |
| 32 | - | code, pre, kbd, samp { font: inherit; } | |
| 33 | - | a { color: inherit; text-decoration: underline; text-decoration-color: var(--rule); text-underline-offset: 3px; } | |
| 34 | - | a:hover { text-decoration-color: currentColor; } | |
| 35 | - | hr { border: 0; border-top: 1px solid var(--rule); margin: var(--lh) 0; } | |
| 36 | - | ::selection { background: var(--ink); color: var(--paper); } | |
| 40 | + | code, pre, kbd, samp { font-family: var(--mono); } | |
| 41 | + | a { color: inherit; text-decoration: underline; text-decoration-color: var(--border-strong); text-underline-offset: 3px; } | |
| 42 | + | a:hover { text-decoration-color: currentColor; color: var(--ink-hi); } | |
| 43 | + | hr { border: 0; border-top: 1px solid var(--border); margin: var(--s5) 0; } | |
| 44 | + | ::selection { background: var(--ink-hi); color: var(--on-ink); } | |
| 37 | 45 | ||
| 38 | 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 | 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 | 57 | .skip { | |
| 44 | - | position: absolute; left: 0; top: 0; z-index: 30; padding: 0 1ch; | |
| 45 | - | background: var(--ink); color: var(--paper); | |
| 46 | - | transform: translateY(-100%); | |
| 58 | + | position: absolute; left: var(--s2); top: var(--s2); z-index: 30; padding: var(--s1) var(--s3); | |
| 59 | + | background: var(--ink-hi); color: var(--on-ink); border-radius: var(--r-s); | |
| 60 | + | transform: translateY(-200%); | |
| 47 | 61 | } | |
| 48 | 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 | 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 | 75 | .nowrap { white-space: nowrap; } | |
| 61 | 76 | .num { text-align: right; font-variant-numeric: tabular-nums; } | |
| 62 | 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 | 1 | @layer components { | |
| 6 | - | /* neferbar: black bars; one accent block (top left) with a 4-cell stepped fade. | |
| 7 | - | The fade is four hard-edged 1ch cells (not a blend): it is the bar's own glyph-grid look. */ | |
| 8 | - | .bar { | |
| 9 | - | display: flex; align-items: stretch; column-gap: 1ch; | |
| 10 | - | height: var(--bar-h); line-height: var(--bar-h); | |
| 11 | - | background: var(--bar); color: var(--bar-ink); | |
| 12 | - | white-space: nowrap; overflow: hidden; | |
| 13 | - | position: sticky; z-index: 20; | |
| 14 | - | & a { text-decoration: none; } | |
| 15 | - | & a:hover { text-decoration: underline; } | |
| 16 | - | & :focus-visible { outline-color: var(--accent); outline-offset: -2px; } | |
| 17 | - | & .blk { background: var(--accent); color: var(--on-accent); font-weight: bold; padding: 0 1ch; } | |
| 18 | - | & .fade { | |
| 19 | - | flex: none; width: 4ch; margin-left: -1ch; | |
| 20 | - | background: linear-gradient(to right, | |
| 21 | - | var(--fade-1) 0 25%, var(--fade-2) 25% 50%, var(--fade-3) 50% 75%, var(--fade-4) 75%); | |
| 22 | - | } | |
| 2 | + | /* buttons: one shape for links and buttons that act */ | |
| 3 | + | .btn { | |
| 4 | + | display: inline-flex; align-items: center; gap: var(--s2); white-space: nowrap; | |
| 5 | + | height: 32px; padding: 0 var(--s3); | |
| 6 | + | font: 600 var(--fs-s) / 1 var(--font); color: var(--ink); text-decoration: none; | |
| 7 | + | background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: var(--r-s); | |
| 8 | + | cursor: pointer; | |
| 9 | + | &:hover { background: var(--bg-strong); color: var(--ink-hi); text-decoration: none; } | |
| 10 | + | & kbd { margin-right: -4px; } | |
| 11 | + | &.primary { background: var(--ink-hi); color: var(--on-ink); border-color: var(--ink-hi); } | |
| 12 | + | &.primary:hover { background: var(--ink); border-color: var(--ink); color: var(--on-ink); } | |
| 13 | + | &.danger:hover { color: var(--red); border-color: var(--red); } | |
| 14 | + | &.icon { width: 32px; justify-content: center; padding: 0; } | |
| 15 | + | &[aria-disabled="true"] { opacity: .45; pointer-events: none; } | |
| 16 | + | &[aria-current="true"], &[aria-pressed="true"] { background: var(--bg-strong); color: var(--ink-hi); border-color: var(--ink-mute); } | |
| 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 { | |
| 26 | - | top: 0; | |
| 27 | - | & .crumbs ol { display: flex; } | |
| 28 | - | & .crumbs li + li::before { content: "/"; color: var(--bar-dim); padding: 0 .5ch; } | |
| 29 | - | & .crumbs [aria-current] { color: #fff; font-weight: bold; } | |
| 30 | - | & .secs { margin-left: 2ch; overflow: hidden; } | |
| 31 | - | & .secs ul { display: flex; } | |
| 32 | - | & .secs li + li::before { content: "/"; color: var(--bar-dim); } | |
| 33 | - | & .secs a { display: inline-block; padding: 0 1ch; color: var(--bar-dim); } | |
| 34 | - | & .secs a:hover { color: var(--bar-ink); } | |
| 35 | - | & .secs a::first-letter { text-decoration: underline; } | |
| 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; } | |
| 24 | + | .pill { | |
| 25 | + | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | |
| 26 | + | font: 600 var(--fs-xs) / 1 var(--mono); color: var(--ink-dim); vertical-align: 1px; | |
| 27 | + | background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: 999px; white-space: nowrap; | |
| 28 | + | &.tag::before { content: ""; width: 6px; height: 6px; margin-right: 6px; border: 1.5px solid currentColor; border-radius: 1px; transform: rotate(45deg); } | |
| 29 | + | &.new { color: var(--teal); border-color: currentColor; } | |
| 30 | + | } | |
| 31 | + | .count { | |
| 32 | + | display: inline-block; min-width: 22px; padding: 0 7px; margin-left: var(--s1); | |
| 33 | + | font: 600 var(--fs-xs) / 20px var(--font); text-align: center; color: var(--ink-dim); | |
| 34 | + | background: var(--bg-raised); border-radius: 999px; vertical-align: 1px; | |
| 41 | 35 | } | |
| 42 | 36 | ||
| 43 | - | .status { | |
| 44 | - | bottom: 0; font-size: var(--fs-s); | |
| 45 | - | padding: 0 1ch; | |
| 46 | - | & > span, & .st-cmd { color: var(--bar-dim); } | |
| 47 | - | & .mode { color: #fff; font-weight: bold; } | |
| 48 | - | & .st-ctx { color: var(--bar-ink); } | |
| 49 | - | & .st-cmd { flex: none; } | |
| 50 | - | & .st-cmd .hi, & .st-cmd:hover { color: #fff; font-weight: bold; } | |
| 51 | - | & .st-sel { color: var(--bar-ink); } | |
| 52 | - | & .st-flash:empty { display: none; } | |
| 53 | - | & .st-flash { color: #fff; font-weight: bold; } | |
| 54 | - | & .st-hints { margin-left: auto; overflow: hidden; text-overflow: ellipsis; min-width: 0; } | |
| 55 | - | & .st-live::before { content: "○ "; } | |
| 56 | - | & .st-live[data-state="live"] { color: var(--bar-ink); } | |
| 57 | - | & .st-live[data-state="live"]::before { content: "● "; } | |
| 58 | - | & .st-owner { flex: none; display: flex; gap: 1ch; } | |
| 59 | - | & .st-owner span { color: var(--bar-ink); } | |
| 60 | - | & .st-owner button { color: var(--bar-dim); } | |
| 61 | - | & .st-owner button:hover { color: #fff; text-decoration: underline; } | |
| 62 | - | & .st-pos { min-width: 7ch; text-align: right; color: var(--bar-ink); } | |
| 37 | + | /* header: identity, path and actions on top, the repository tabs below. | |
| 38 | + | --hdr-h is its height, the offset of the sticky bars of the diffs. */ | |
| 39 | + | body { --hdr-h: 57px; } | |
| 40 | + | body:has(.hdr .tabs) { --hdr-h: 101px; } | |
| 41 | + | .hdr { | |
| 42 | + | position: sticky; top: 0; z-index: 20; | |
| 43 | + | background: var(--bg-surface); border-bottom: 1px solid var(--border); | |
| 44 | + | } | |
| 45 | + | .hdr-top { | |
| 46 | + | display: flex; align-items: center; gap: var(--s4); | |
| 47 | + | max-width: var(--page); margin: 0 auto; height: 56px; padding: 0 var(--pad); | |
| 48 | + | } | |
| 49 | + | .brand { | |
| 50 | + | display: flex; align-items: center; gap: var(--s2); flex: none; | |
| 51 | + | font: 700 var(--fs) / 1 var(--mono); color: var(--ink-hi); text-decoration: none; | |
| 52 | + | & span { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 53 | + | &:hover { text-decoration: none; } | |
| 54 | + | } | |
| 55 | + | .brand-mark { | |
| 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. | |
| 66 | - | Tables and code take the free width and the rail is fixed; on prose pages the text | |
| 67 | - | is capped at the reading measure and the rail takes the rest, so no column is idle. */ | |
| 68 | - | .work { min-width: 0; } | |
| 69 | - | .rail { padding: 0 var(--pad) var(--lh); min-width: 0; } | |
| 70 | - | @media (min-width: 1100px) { | |
| 71 | - | /* app shell: the bars stay put, the content and the rail scroll on their own, so each | |
| 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; | |
| 86 | + | .tabs { | |
| 87 | + | max-width: var(--page); margin: 0 auto; padding: 0 var(--pad); | |
| 88 | + | overflow-x: auto; scrollbar-width: none; | |
| 89 | + | & ul { display: flex; gap: var(--s1); } | |
| 90 | + | & a { | |
| 91 | + | position: relative; display: flex; align-items: center; gap: var(--s2); height: 44px; padding: 0 var(--s3); | |
| 92 | + | color: var(--ink-dim); text-decoration: none; white-space: nowrap; font-weight: 600; | |
| 90 | 93 | } | |
| 91 | - | .rail-d > summary::-webkit-details-marker { display: none; } | |
| 92 | - | .rail-d > summary::before { content: "▸ "; color: var(--ink); } | |
| 93 | - | .rail-d[open] > summary::before { content: "▾ "; } | |
| 94 | - | .rail-d[open] { padding-bottom: var(--lh); } | |
| 95 | - | } | |
| 96 | - | @media (max-width: 700px) { | |
| 97 | - | /* the sections drop to a second bar row instead of being cut off */ | |
| 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; } | |
| 94 | + | & a::after { content: ""; position: absolute; left: var(--s2); right: var(--s2); bottom: -1px; height: 2px; border-radius: 2px; } | |
| 95 | + | & a:hover { color: var(--ink-hi); } | |
| 96 | + | & a:hover::after { background: var(--border-strong); } | |
| 97 | + | & a[aria-current] { color: var(--ink-hi); } | |
| 98 | + | & a[aria-current]::after { background: var(--ink-hi); } | |
| 99 | + | & kbd { opacity: .55; } | |
| 100 | + | & a:hover kbd, & a:focus-visible kbd, & a[aria-current] kbd { opacity: 1; } | |
| 103 | 101 | } | |
| 104 | 102 | ||
| 105 | - | /* rail blocks: a dim title rule, then key/value rows on the character grid */ | |
| 106 | - | .rail section { margin-top: var(--lh); } | |
| 107 | - | .rail h2 { | |
| 108 | - | display: flex; gap: 1ch; color: var(--ink-dim); font-weight: normal; | |
| 109 | - | &::after { content: ""; flex: 1; align-self: center; border-top: 1px solid var(--rule); } | |
| 103 | + | /* work area: content and rail, centred, the page scrolls */ | |
| 104 | + | .work { | |
| 105 | + | flex: 1; width: 100%; max-width: var(--page); margin: 0 auto; | |
| 106 | + | padding: var(--s5) var(--pad) var(--s7); | |
| 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 | 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 | 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 | 135 | .outline { | |
| 117 | - | & a { display: block; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 118 | - | & a:hover { text-decoration: underline; } | |
| 119 | - | & .l1 { font-weight: bold; } | |
| 120 | - | & .l3 { padding-left: 2ch; } | |
| 121 | - | & .l4, & .l5, & .l6 { padding-left: 4ch; } | |
| 122 | - | & a[aria-current]::before { content: "▸"; margin-left: -1ch; } | |
| 123 | - | } | |
| 124 | - | .rail .clone { border: 0; padding: 0; margin-top: 0; } | |
| 125 | - | .rail .clone-line { flex-wrap: wrap; } | |
| 136 | + | font-size: var(--fs-s); | |
| 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; } | |
| 138 | + | & a:hover { background: var(--bg-raised); color: var(--ink-hi); } | |
| 139 | + | & .l1 { font-weight: 700; color: var(--ink); } | |
| 140 | + | & .l3 { padding-left: var(--s5); } | |
| 141 | + | & .l4, & .l5, & .l6 { padding-left: var(--s6); } | |
| 142 | + | } | |
| 143 | + | ||
| 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 | 1 | @layer components { | |
| 2 | - | /* bottom panels above the status bar (help, ref switcher, ":" command line) */ | |
| 3 | - | .panel { | |
| 4 | - | position: fixed; inset: auto 0 var(--bar-h) 0; margin: 0; width: 100%; z-index: 15; | |
| 5 | - | background: var(--paper); color: var(--ink); | |
| 6 | - | border-top: 2px solid var(--ink); | |
| 7 | - | max-height: min(46dvh, 28rem); overflow: auto; overscroll-behavior: contain; | |
| 8 | - | padding: 0 var(--pad) var(--lh); | |
| 9 | - | &:not([open]) { display: none; } | |
| 10 | - | & .panel-head { | |
| 11 | - | position: sticky; top: 0; background: var(--paper); display: flex; gap: 1ch; | |
| 12 | - | align-items: baseline; height: calc(var(--lh) + 6px); padding-top: 4px; | |
| 13 | - | border-bottom: 1px solid var(--rule); margin-bottom: 4px; | |
| 14 | - | & input { flex: 1; min-width: 0; outline-offset: 2px; } | |
| 15 | - | & button { margin-left: auto; color: var(--ink-dim); white-space: nowrap; } | |
| 16 | - | & button:hover { color: var(--ink); text-decoration: underline; } | |
| 17 | - | & .hi { font-weight: bold; } | |
| 2 | + | /* dialogs: the "go to" palette, the ref switcher and the keys, centred over a backdrop */ | |
| 3 | + | .modal { | |
| 4 | + | position: fixed; inset: 0; margin: 12vh auto auto; z-index: 40; | |
| 5 | + | width: min(640px, calc(100vw - 2 * var(--s4))); max-height: min(70dvh, 560px); | |
| 6 | + | display: none; flex-direction: column; | |
| 7 | + | background: var(--bg-overlay); color: var(--ink); | |
| 8 | + | border: 1px solid var(--border-strong); border-radius: var(--r); box-shadow: var(--shadow); | |
| 9 | + | overflow: hidden; overscroll-behavior: contain; | |
| 10 | + | &[open] { display: flex; } | |
| 11 | + | &::backdrop { background: var(--backdrop); } | |
| 12 | + | &#panel-help { width: min(820px, calc(100vw - 2 * var(--s4))); } | |
| 13 | + | } | |
| 14 | + | @media (prefers-reduced-motion: no-preference) { | |
| 15 | + | .modal[open] { animation: modal-in 120ms ease-out; } | |
| 16 | + | @keyframes modal-in { from { opacity: 0; transform: translateY(-6px) scale(.99); } } | |
| 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; } | |
| 20 | - | & .panel-cols h2 { color: var(--ink-dim); font-weight: normal; } | |
| 21 | - | & li a { display: block; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| 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; } | |
| 43 | + | & li a:hover, & li a:focus-visible { background: var(--bg-raised); color: var(--ink-hi); } | |
| 44 | + | & li a[aria-current] { font-weight: 700; color: var(--ink-hi); } | |
| 45 | + | & .mark { display: inline-block; width: 1ch; color: var(--teal); } | |
| 25 | 46 | } | |
| 47 | + | ||
| 26 | 48 | .cmd-list { | |
| 27 | - | columns: 34ch; column-gap: 3ch; | |
| 28 | - | & li { break-inside: avoid; } | |
| 29 | - | & li[data-cursor] a { background: var(--row); } | |
| 30 | - | & li[data-cursor] a::before { content: "▸"; position: absolute; margin-left: -1.5ch; } | |
| 31 | - | & a { padding-left: 1.5ch; } | |
| 32 | - | & .k { display: inline-block; width: 8ch; color: var(--ink-mute); } | |
| 49 | + | & li a { | |
| 50 | + | display: flex; align-items: center; gap: var(--s3); padding: var(--s2) var(--s3); border-radius: var(--r-s); | |
| 51 | + | text-decoration: none; color: var(--ink); | |
| 52 | + | } | |
| 53 | + | & li a:hover { background: var(--bg-raised); } | |
| 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 | 59 | .keys { | |
| 35 | - | display: grid; grid-template-columns: repeat(auto-fill, minmax(46ch, 1fr)); column-gap: 3ch; | |
| 36 | - | & > div { display: grid; grid-template-columns: 14ch 1fr; } | |
| 37 | - | & dt { white-space: nowrap; } | |
| 38 | - | & dd { color: var(--ink-dim); } | |
| 39 | - | & kbd { border: 1px solid var(--rule); padding: 0 .6ch; } | |
| 60 | + | display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); column-gap: var(--s5); row-gap: var(--s1); | |
| 61 | + | padding: var(--s3) var(--s4) var(--s4) !important; | |
| 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); } | |
| 63 | + | & dt { white-space: nowrap; display: flex; gap: var(--s1); } | |
| 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 | 1 | @layer components { | |
| 2 | - | /* plain-text tables with a ▸ row cursor */ | |
| 2 | + | /* tables in a card, with a row cursor (j/k) */ | |
| 3 | 3 | .list { | |
| 4 | 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 | 12 | & th a { text-decoration: none; } | |
| 7 | - | & th a:hover { text-decoration: underline; color: var(--ink); } | |
| 8 | - | & th[aria-sort="ascending"], & th[aria-sort="descending"] { color: var(--ink); font-weight: bold; } | |
| 9 | - | & td, & th { padding-right: 2ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 10 | - | & .cur { width: 2ch; padding: 0; } | |
| 11 | - | & tbody tr[data-cursor] { background: var(--row); } | |
| 12 | - | & tbody tr[data-cursor] .cur::before { content: "▸"; font-weight: bold; } | |
| 13 | - | & tbody tr:hover { background: var(--row); } | |
| 13 | + | & th a:hover { color: var(--ink-hi); } | |
| 14 | + | & th[aria-sort="ascending"], & th[aria-sort="descending"] { color: var(--ink-hi); } | |
| 15 | + | & td { height: 40px; padding: 0 var(--s3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-top: 1px solid var(--border); } | |
| 16 | + | & tbody tr:first-child td { border-top: 0; } | |
| 17 | + | & th.cur, & td.cur { width: 3px; padding: 0; } | |
| 18 | + | & tbody tr:hover { background: var(--bg-raised); } | |
| 19 | + | & tbody tr[data-cursor] { background: var(--bg-raised); } | |
| 20 | + | & tbody tr[data-cursor] .cur { background: var(--ink-hi); } | |
| 14 | 21 | & tbody tr[hidden] { display: none; } | |
| 15 | 22 | & td > a { text-decoration: none; } | |
| 23 | + | & td > a:first-child { color: var(--ink-hi); } | |
| 16 | 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 | 28 | & .c-sub { color: var(--ink-dim); } | |
| 18 | - | & .c-sub .refs { color: var(--ink); } | |
| 19 | 29 | } | |
| 20 | 30 | ||
| 21 | 31 | .home { | |
| 22 | - | & .c-name { width: 24ch; font-weight: bold; } | |
| 23 | - | & th.c-name { font-weight: normal; } | |
| 24 | - | & th.c-name[aria-sort] { font-weight: bold; } | |
| 32 | + | & .c-name { width: 26%; font-family: var(--mono); font-weight: 700; } | |
| 33 | + | & th.c-name { font-family: var(--font); } | |
| 25 | 34 | & .c-desc { width: auto; } | |
| 26 | - | & .c-when { width: 13ch; } | |
| 27 | - | & .c-ref { width: 10ch; } | |
| 28 | - | & .c-lang { width: 12ch; padding-right: 0; } | |
| 35 | + | & .c-when { width: 130px; } | |
| 36 | + | & .c-ref { width: 120px; } | |
| 37 | + | & .c-lang { width: 120px; } | |
| 29 | 38 | } | |
| 30 | 39 | .list.tree { | |
| 31 | - | & .c-kind { width: 2ch; color: var(--ink-mute); padding-right: 0; } | |
| 32 | - | & .c-name { width: 34ch; } | |
| 33 | - | & .c-size { width: 9ch; text-align: right; padding-right: 3ch; } | |
| 34 | - | & .c-when { width: 11ch; } | |
| 40 | + | & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; } | |
| 41 | + | & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); } | |
| 42 | + | & .c-size { width: 90px; text-align: right; } | |
| 43 | + | & .c-when { width: 100px; } | |
| 35 | 44 | & .c-sub { width: auto; } | |
| 36 | - | & .c-who { width: 12ch; } | |
| 45 | + | & .c-who { width: 130px; } | |
| 37 | 46 | } | |
| 38 | 47 | .list.commits, .list.refs { | |
| 39 | - | & .c-sha { width: 8ch; } | |
| 40 | - | & .c-who { width: 12ch; } | |
| 41 | - | & .c-when { width: 11ch; } | |
| 48 | + | & .c-sha { width: 96px; } | |
| 49 | + | & .c-who { width: 140px; } | |
| 50 | + | & .c-when { width: 100px; } | |
| 42 | 51 | } | |
| 43 | 52 | ||
| 44 | - | /* filter line (home, tree, commits) */ | |
| 53 | + | /* filter: a search field above a list */ | |
| 45 | 54 | .filter { | |
| 46 | - | display: flex; gap: 1ch; height: var(--lh); margin: var(--lh) 0 0; | |
| 47 | - | & label { color: var(--ink-mute); } | |
| 48 | - | & input { flex: 1; min-width: 0; } | |
| 55 | + | position: relative; display: flex; max-width: 420px; | |
| 56 | + | &::before { | |
| 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 | 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 | 75 | .rule-h { | |
| 54 | - | display: flex; gap: 1ch; margin: var(--lh) 0 0; color: var(--ink-dim); | |
| 55 | - | &::after { content: ""; flex: 1; align-self: center; border-top: 1px solid var(--rule); } | |
| 56 | - | & > :first-child { font-weight: bold; color: var(--ink); } | |
| 76 | + | display: flex; align-items: center; gap: var(--s2); margin-top: var(--s5); | |
| 77 | + | font-size: var(--fs); | |
| 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 | 83 | .state { | |
| 61 | - | margin-top: var(--lh); max-width: var(--measure); | |
| 62 | - | & h1 { margin-bottom: var(--lh); } | |
| 63 | - | & .code { display: inline-block; min-width: 6ch; } | |
| 64 | - | & .state-back { margin-top: var(--lh); display: flex; gap: 2ch; } | |
| 65 | - | & .state-act { margin-top: var(--lh); } | |
| 66 | - | & .state-act button { color: var(--ink-hi); font-weight: bold; } | |
| 67 | - | & .state-act button:hover { text-decoration: underline; } | |
| 84 | + | max-width: 560px; margin: var(--s7) auto 0 !important; padding: var(--s6); | |
| 85 | + | background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); | |
| 86 | + | & .eyebrow { font: 700 var(--fs-s) / 1 var(--mono); color: var(--ink-mute); text-transform: uppercase; letter-spacing: .08em; margin-bottom: var(--s2); } | |
| 87 | + | & h1 { margin-bottom: var(--s3); } | |
| 88 | + | & p.dim { color: var(--ink-dim); } | |
| 89 | + | & .state-act { margin-top: var(--s5); } | |
| 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 | 95 | .clone { | |
| 70 | - | margin: var(--lh) 0 0; border: 1px solid var(--rule); padding: 0 1ch; | |
| 71 | - | & .clone-line { display: flex; gap: 1ch; align-items: baseline; } | |
| 72 | - | & code { flex: 1; min-width: 0; overflow-x: auto; white-space: pre; } | |
| 73 | - | & .copy { color: var(--ink-dim); white-space: nowrap; min-width: 9ch; text-align: right; } | |
| 74 | - | & .copy:hover { color: var(--ink); text-decoration: underline; } | |
| 96 | + | display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s2); min-width: 0; | |
| 97 | + | & .clone-line { | |
| 98 | + | min-width: 0; | |
| 99 | + | display: flex; gap: var(--s2); align-items: center; padding: var(--s1) var(--s1) var(--s1) var(--s3); | |
| 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 | 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 | 5 | .facts { | |
| 6 | - | margin-top: var(--lh); display: grid; grid-template-columns: repeat(auto-fill, minmax(38ch, 1fr)); column-gap: 3ch; | |
| 7 | - | & > div { display: grid; grid-template-columns: 12ch 1fr; min-width: 0; } | |
| 6 | + | display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); column-gap: var(--s5); | |
| 7 | + | & > div { display: grid; grid-template-columns: 120px 1fr; min-width: 0; } | |
| 8 | 8 | & dt { color: var(--ink-mute); } | |
| 9 | 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 | 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 | 18 | .prose { | |
| 17 | - | max-width: var(--measure); margin-top: var(--lh); | |
| 18 | - | & h1, & h2, & h3, & h4 { margin: var(--lh) 0 0; } | |
| 19 | - | & h1::before { content: "# "; color: var(--ink-mute); } | |
| 20 | - | & h2::before { content: "## "; color: var(--ink-mute); } | |
| 21 | - | & h3::before { content: "### "; color: var(--ink-mute); } | |
| 22 | - | & h4::before { content: "#### "; color: var(--ink-mute); } | |
| 19 | + | max-width: var(--measure); line-height: 1.65; overflow-wrap: break-word; | |
| 20 | + | & h1, & h2, & h3, & h4 { margin: 1.6em 0 0; } | |
| 21 | + | & h1 { font-size: 26px; line-height: 34px; padding-bottom: var(--s2); border-bottom: 1px solid var(--border); } | |
| 22 | + | & h2 { font-size: 20px; line-height: 28px; padding-bottom: var(--s1); border-bottom: 1px solid var(--border); } | |
| 23 | + | & h3 { font-size: 17px; } | |
| 23 | 24 | & > :first-child { margin-top: 0; } | |
| 24 | - | & p, & ul, & ol, & pre, & table, & blockquote { margin-top: var(--lh); } | |
| 25 | - | & ul { list-style: none; & > li::before { content: "- "; color: var(--ink-mute); margin-left: -2ch; } } | |
| 26 | - | & ul, & ol { padding-left: 2ch; } | |
| 25 | + | & p, & ul, & ol, & pre, & table, & blockquote { margin-top: var(--s4); } | |
| 26 | + | & ul { list-style: disc; } | |
| 27 | + | & ul, & ol { padding-left: var(--s5); } | |
| 27 | 28 | & ol { list-style: decimal; } | |
| 28 | - | & li > ul, & li > ol, & li > p { margin-top: 0; } | |
| 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 | - | & :not(pre) > code { border-bottom: 1px dotted var(--ink-mute); overflow-wrap: anywhere; } | |
| 31 | - | & blockquote { padding-left: 2ch; border-left: 1px solid var(--rule); color: var(--ink-dim); } | |
| 32 | - | & table { border-collapse: collapse; display: block; overflow-x: auto; } | |
| 33 | - | & th, & td { padding: 0 2ch 0 0; white-space: nowrap; vertical-align: top; } | |
| 34 | - | & th { border-bottom: 1px solid var(--rule); } | |
| 35 | - | & hr { margin: var(--lh) 0; } | |
| 29 | + | & li + li { margin-top: var(--s1); } | |
| 30 | + | & li::marker { color: var(--ink-mute); } | |
| 31 | + | & li > ul, & li > ol, & li > p { margin-top: var(--s1); } | |
| 32 | + | & pre { | |
| 33 | + | padding: var(--s3) var(--s4); overflow-x: auto; | |
| 34 | + | background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--r-s); | |
| 35 | + | font-size: var(--fs-code); line-height: var(--lh-code); | |
| 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 | 47 | & .alt { color: var(--ink-mute); } | |
| 37 | 48 | & img { display: none; } | |
| 38 | 49 | } | |
M internal/adapters/web/static/css/50-code.css +48 -39
| @@ -1,17 +1,21 @@ | |||
| 1 | 1 | @layer components { | |
| 2 | 2 | /* page title for a file: name, then its actions as plain words */ | |
| 3 | 3 | .file-h { | |
| 4 | - | display: flex; gap: 2ch; align-items: baseline; margin-top: var(--lh); | |
| 5 | - | padding-bottom: 2px; border-bottom: 1px solid var(--rule); | |
| 6 | - | & .acts { margin-left: auto; font-weight: normal; display: flex; gap: 2ch; } | |
| 4 | + | display: flex; gap: var(--s4); align-items: center; flex-wrap: wrap; font-size: 18px; | |
| 5 | + | & > span:first-child { overflow-wrap: anywhere; } | |
| 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); } | |
| 9 | - | .note { color: var(--ink-dim); } | |
| 10 | - | .pad { padding: 0 1ch; } | |
| 8 | + | .notice { | |
| 9 | + | padding: var(--s3) var(--s4); max-width: var(--measure); font-size: var(--fs-s); | |
| 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 | 15 | /* source view: gutter numbers are links (selectable lines), lines span the full width */ | |
| 13 | 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 | 19 | font-size: var(--fs-code); line-height: var(--lh-code); | |
| 16 | 20 | & code { display: grid; min-width: max-content; } | |
| 17 | 21 | & .line { padding-right: var(--pad); border-left: 2px solid transparent; } | |
| @@ -23,44 +27,43 @@ | |||
| 23 | 27 | } | |
| 24 | 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 | 34 | /* commit header, like `git show` */ | |
| 29 | 35 | .show { | |
| 30 | - | margin-top: var(--lh); | |
| 31 | - | & h1 { max-width: var(--measure); } | |
| 32 | - | & .msg { margin-top: var(--lh); white-space: pre-wrap; max-width: var(--measure); color: var(--ink-dim); } | |
| 33 | - | & .show-kv { margin-top: var(--lh); } | |
| 34 | - | & .show-kv dd { white-space: normal; } | |
| 36 | + | padding: var(--s5); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); | |
| 37 | + | & h1 { max-width: var(--measure); overflow-wrap: anywhere; } | |
| 38 | + | & .msg { margin-top: var(--s3); white-space: pre-wrap; max-width: var(--measure); color: var(--ink-dim); font-size: var(--fs-s); } | |
| 39 | + | & .show-kv { margin-top: var(--s4); } | |
| 40 | + | & .show-kv dd { white-space: normal; overflow-wrap: anywhere; } | |
| 35 | 41 | } | |
| 36 | 42 | .diff-tools { | |
| 37 | - | display: flex; gap: 2ch; align-items: baseline; margin-top: var(--lh); | |
| 38 | - | position: sticky; top: var(--stick); z-index: 5; background: var(--paper); | |
| 39 | - | border-bottom: 1px solid var(--rule); | |
| 43 | + | display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; | |
| 44 | + | position: sticky; top: var(--hdr-h, 0px); z-index: 5; padding: var(--s2) 0; background: var(--bg-canvas); | |
| 40 | 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 | 48 | /* one file of a diff */ | |
| 48 | 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 | 51 | & > summary { | |
| 51 | - | position: sticky; top: calc(var(--stick) + var(--lh) + 1px); z-index: 4; | |
| 52 | - | background: var(--paper-alt); border: 1px solid var(--rule); padding: 0 1ch; | |
| 52 | + | position: sticky; top: calc(var(--hdr-h, 0px) + 48px); z-index: 4; | |
| 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 | 55 | white-space: nowrap; overflow: hidden; text-overflow: ellipsis; list-style: none; | |
| 54 | 56 | } | |
| 57 | + | &:not([open]) > summary { border-radius: var(--r); } | |
| 55 | 58 | & > summary::-webkit-details-marker { display: none; } | |
| 56 | 59 | & > summary::before { content: "▸ "; } | |
| 57 | 60 | &[open] > summary::before { content: "▾ "; } | |
| 58 | 61 | & .st { font-weight: bold; } | |
| 59 | 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 | 65 | .dt { | |
| 63 | - | width: max-content; min-width: 100%; | |
| 66 | + | width: max-content; min-width: 100%; font-family: var(--mono); | |
| 64 | 67 | font-size: var(--fs-code); line-height: var(--lh-code); | |
| 65 | 68 | & td { padding: 0; vertical-align: top; } | |
| 66 | 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 | 78 | & tr.del .ln, & .ln.del { color: var(--red); } | |
| 76 | 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 | 82 | .spl { width: 100%; table-layout: fixed; & .tx { overflow: hidden; text-overflow: ellipsis; } & .ln { width: 6ch; } & .m { width: 2ch; } } | |
| 80 | 83 | #diffs[data-diff="unified"] .spl, #diffs[data-diff="split"] .uni { display: none; } | |
| 81 | 84 | ||
| 82 | 85 | /* diff table of contents in the rail */ | |
| 83 | 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 | 90 | & a:hover .p { text-decoration: underline; } | |
| 86 | 91 | & a[aria-current="true"] { background: var(--row); font-weight: bold; } | |
| 87 | 92 | & .p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; } | |
| @@ -90,26 +95,30 @@ | |||
| 90 | 95 | } | |
| 91 | 96 | ||
| 92 | 97 | @media (max-width: 700px) { | |
| 93 | - | .diff-tools { flex-wrap: wrap; column-gap: 1ch; } | |
| 94 | 98 | .diff-tools .grow { flex-basis: 100%; height: 0; } | |
| 95 | - | .show-kv dd { overflow-wrap: anywhere; } | |
| 96 | - | .file-h .acts { gap: 1ch; } | |
| 99 | + | .file-h .acts { margin-left: 0; } | |
| 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 | 102 | .days section[hidden] { display: none; } | |
| 100 | 103 | .list.refs { | |
| 101 | - | & .c-kind { width: 8ch; } | |
| 102 | - | & .c-name { width: 34ch; } | |
| 103 | - | & .c-info { width: 11ch; } | |
| 104 | - | & .c-sha { width: 9ch; } | |
| 104 | + | & .c-kind { width: 80px; color: var(--ink-mute); } | |
| 105 | + | & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); } | |
| 106 | + | & .c-sha { width: 96px; } | |
| 105 | 107 | & .c-sub { width: auto; } | |
| 106 | 108 | } | |
| 109 | + | /* the commit table inside a rail block: no frame of its own */ | |
| 107 | 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 | 116 | & .c-who { display: none; } | |
| 109 | - | & .c-sha { width: 8ch; } | |
| 110 | - | & .c-when { width: 9ch; padding-right: 0; text-align: right; } | |
| 117 | + | & .c-sha { width: 72px; } | |
| 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 | 122 | .rail section > p { overflow-wrap: anywhere; } | |
| 114 | 123 | @media (max-width: 700px) { | |
| 115 | 124 | /* the name takes what the fixed columns leave */ | |
| @@ -118,7 +127,7 @@ | |||
| 118 | 127 | } | |
| 119 | 128 | ||
| 120 | 129 | @layer components { | |
| 121 | - | .gallery section { margin-top: calc(var(--lh) * 2); } | |
| 130 | + | .gallery section { margin-top: var(--s6); } | |
| 122 | 131 | .gallery .rule-h > :last-child { font-weight: normal; color: var(--ink-mute); order: 2; } | |
| 123 | 132 | .gallery .rule-h::after { order: 1; } | |
| 124 | 133 | .gallery .files { max-width: var(--rail); } | |
M internal/adapters/web/static/css/55-diff.css +1 -6
| @@ -2,12 +2,7 @@ | |||
| 2 | 2 | /* an invisible character in code, shown instead of applied (Trojan Source) */ | |
| 3 | 3 | .inv { | |
| 4 | 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 | 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 | 1 | @layer components { | |
| 2 | 2 | /* merge requests: the review page. Hue only where it carries a verdict. */ | |
| 3 | 3 | .mr-h .show-kv { margin-top: 0; } | |
| 4 | - | .mr-h + * { margin-top: var(--lh); } | |
| 4 | + | ||
| 5 | 5 | .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); } | |
| 6 | 6 | .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); } | |
| 7 | 7 | .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); } | |
| 8 | - | .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 | 9 | .mr-desc a, .c-text a { text-decoration: underline; } | |
| 10 | 10 | ||
| 11 | 11 | .since { | |
| 12 | - | display: flex; flex-wrap: wrap; column-gap: 3ch; margin-top: var(--lh); | |
| 13 | - | & a { text-decoration: none; color: var(--ink-dim); } | |
| 14 | - | & a:hover { color: var(--ink); text-decoration: underline; } | |
| 15 | - | & a[aria-current] { color: var(--ink-hi); font-weight: bold; } | |
| 16 | - | & a[aria-current]::before { content: "▸ "; } | |
| 17 | - | & .mute { font-weight: normal; } | |
| 12 | + | display: inline-flex; flex-wrap: wrap; gap: var(--s1); padding: var(--s1); | |
| 13 | + | background: var(--bg-raised); border-radius: var(--r-s); | |
| 14 | + | & a { padding: var(--s1) var(--s3); border-radius: 4px; text-decoration: none; color: var(--ink-dim); font-size: var(--fs-s); } | |
| 15 | + | & a:hover { color: var(--ink-hi); } | |
| 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: 400; font-family: var(--mono); } | |
| 18 | 18 | } | |
| 19 | 19 | ||
| 20 | 20 | /* merge block: first thing in the rail */ | |
| 21 | 21 | .decide { | |
| 22 | - | & .d-acts { margin-top: calc(var(--lh) / 2); } | |
| 22 | + | border-color: var(--border-strong) !important; | |
| 23 | 23 | & select { | |
| 24 | - | display: block; width: 100%; font: inherit; color: inherit; background: var(--paper-alt); | |
| 25 | - | border: 1px solid var(--rule); border-radius: 0; padding: 0 1ch; | |
| 24 | + | display: block; width: 100%; height: 36px; padding: 0 var(--s3); | |
| 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 input { accent-color: var(--ink); margin: 0 1ch 0 0; vertical-align: middle; } | |
| 29 | - | & .d-btns { display: flex; gap: 2ch; margin-top: 4px; flex-wrap: wrap; } | |
| 30 | - | & .d-btns button { color: var(--ink-dim); } | |
| 31 | - | & .d-btns button:hover { color: var(--ink); text-decoration: underline; } | |
| 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); } | |
| 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; } | |
| 29 | + | & .d-check input { accent-color: var(--ink-hi); margin: 0; } | |
| 30 | + | & .d-btns { display: flex; gap: var(--s2); margin-top: var(--s3); flex-wrap: wrap; } | |
| 31 | + | & .d-btns .primary { flex: 1; justify-content: center; } | |
| 32 | + | & .d-or { font-size: var(--fs-s); } | |
| 35 | 33 | & .clone { margin-top: 0; } | |
| 36 | 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; } | |
| 40 | - | .pcommits li[data-new] { background: var(--row); } | |
| 37 | + | .pcommits { font-size: var(--fs-s); display: grid; gap: var(--s1); } | |
| 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 | 41 | .pcommits .s { color: var(--ink); } | |
| 42 | - | .rail code { border-bottom: 1px dotted var(--ink-mute); } | |
| 42 | + | ||
| 43 | 43 | ||
| 44 | 44 | /* a note: a ruled block, under its line in the diff and in the list of comments */ | |
| 45 | 45 | .cmt { | |
| 46 | - | border-left: 2px solid var(--ink); padding-left: 1ch; margin-top: calc(var(--lh) / 2); max-width: var(--measure); | |
| 47 | - | white-space: normal; | |
| 46 | + | padding: var(--s3) var(--s4); margin-top: var(--s3); max-width: var(--measure); | |
| 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 | 49 | & p { white-space: normal; } | |
| 49 | 50 | & .c-who { font-weight: bold; } | |
| 50 | 51 | & .c-state { color: var(--ink-dim); } | |
| 51 | 52 | & .c-text { white-space: pre-wrap; overflow-wrap: anywhere; } | |
| 52 | 53 | & .c-where { font-size: var(--fs-s); overflow-wrap: anywhere; } | |
| 53 | - | & .c-act button { color: var(--ink-dim); } | |
| 54 | - | & .c-act button:hover { color: var(--ink); text-decoration: underline; } | |
| 55 | - | &[data-resolved="true"] { border-left-color: var(--rule); } | |
| 54 | + | & .c-act { margin-top: var(--s2); } | |
| 55 | + | & .c-act button { height: 26px; font-size: var(--fs-xs); } | |
| 56 | + | &[data-resolved="true"] { border-left-color: var(--border-strong); } | |
| 56 | 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 .cmt, .dt .fnote .cform { margin: 0; font-size: var(--fs); line-height: var(--lh); } | |
| 60 | - | .dt .fnote .cmt + .cmt, .dt .fnote .cmt + .cform { margin-top: calc(var(--lh) / 2); } | |
| 59 | + | .dt .fnote td { background: var(--bg-canvas); padding: var(--s2) var(--s3); white-space: normal; } | |
| 60 | + | .dt .fnote .cmt, .dt .fnote .cform { margin: 0; } | |
| 61 | + | .dt .fnote .cmt + .cmt, .dt .fnote .cmt + .cform { margin-top: var(--s2); } | |
| 61 | 62 | .ln a { color: inherit; text-decoration: none; } | |
| 62 | 63 | .ln a:hover, .ln a:focus-visible { text-decoration: underline; color: var(--ink-hi); } | |
| 63 | 64 | ||
| 64 | 65 | /* the forms: a comment, a new merge request */ | |
| 65 | 66 | .cform { | |
| 66 | - | display: grid; gap: 2px; margin-top: var(--lh); max-width: var(--measure); | |
| 67 | - | & label { color: var(--ink-dim); } | |
| 67 | + | display: grid; gap: var(--s2); margin-top: var(--s3); max-width: var(--measure); | |
| 68 | + | font-family: var(--font); font-size: var(--fs-s); | |
| 69 | + | & label { color: var(--ink-dim); font-weight: 600; } | |
| 68 | 70 | & textarea, & input[type="text"] { | |
| 69 | - | display: block; width: 100%; font: inherit; color: inherit; resize: vertical; | |
| 70 | - | background: var(--paper-alt); border: 1px solid var(--rule); border-radius: 0; padding: 0 1ch; | |
| 71 | + | display: block; width: 100%; font: inherit; font-size: var(--fs); color: inherit; resize: vertical; | |
| 72 | + | background: var(--bg-surface); border: 1px solid var(--border-strong); border-radius: var(--r-s); padding: var(--s2) var(--s3); | |
| 71 | 73 | } | |
| 72 | 74 | & textarea::placeholder { color: var(--ink-mute); opacity: 1; } | |
| 73 | - | & .f-btns { display: flex; gap: 2ch; align-items: baseline; margin-top: 4px; } | |
| 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; } | |
| 75 | + | & .f-btns { display: flex; gap: var(--s2); align-items: center; margin-top: var(--s1); } | |
| 79 | 76 | } | |
| 80 | - | .cform.create { margin-top: var(--lh); } | |
| 81 | 77 | ||
| 82 | 78 | .list.mrs { | |
| 83 | - | & .c-n { width: 5ch; } | |
| 84 | - | & .c-state { width: 9ch; } | |
| 79 | + | & .c-n { width: 64px; } | |
| 80 | + | & .c-state { width: 110px; } | |
| 85 | 81 | & .c-subject { overflow: hidden; text-overflow: ellipsis; } | |
| 86 | - | & .c-who { width: 12ch; } | |
| 87 | - | & .c-when { width: 11ch; } | |
| 82 | + | & .c-who { width: 140px; } | |
| 83 | + | & .c-when { width: 100px; } | |
| 88 | 84 | & tr[data-state="open"] .c-state { font-weight: bold; } | |
| 89 | 85 | & tr[data-state="open"] .c-state::before { content: "● "; color: var(--teal); } | |
| 90 | 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 | 1 | @layer components { | |
| 2 | 2 | /* live updates: the banner of new commits, an empty slot takes no room */ | |
| 3 | 3 | #commits-head:empty { display: none; } | |
| 4 | - | .live-banner { margin: var(--lh) 0; padding: 0 1ch; background: var(--row); font-weight: bold; } | |
| 5 | - | .live-banner a { color: var(--ink); } | |
| 4 | + | .live-banner { | |
| 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 | 9 | let pending = 0; | |
| 10 | 10 | const fire = (name) => document.body.dispatchEvent(new CustomEvent('k-' + name, { bubbles: false })); | |
| 11 | 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 | 14 | if (e.ctrlKey || e.metaKey || e.altKey) return; | |
| 13 | 15 | const typing = e.target.closest('input, textarea, select, [contenteditable]'); | |
| 14 | 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 | 73 | l := s.repoLayout(ctx, d, "summary") | |
| 74 | 74 | l.Title = d.name() + " · " + s.instance | |
| 75 | 75 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=summary") | |
| 76 | - | l.Status = Status{ | |
| 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" | |
| 76 | + | l.HasRail = true | |
| 81 | 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 | 103 | } else { | |
| 104 | 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 | 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 | 7 | templ Tree(l Layout, p TreePage) { | |
| 8 | 8 | @Shell(l, nil) { | |
| 9 | 9 | <div class="filter"> | |
| 10 | - | <label for="tree-filter">filter></label> | |
| 10 | + | <label for="tree-filter" class="sr">filter</label> | |
| 11 | 11 | <input | |
| 12 | 12 | id="tree-filter" | |
| 13 | 13 | type="search" | |
| 14 | 14 | data-filter | |
| 15 | - | placeholder="this directory ( / )" | |
| 15 | + | placeholder="Filter this directory…" | |
| 16 | 16 | autocomplete="off" | |
| 17 | 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 | 34 | <p class="note">{ n }</p> | |
| 35 | 35 | } | |
| 36 | 36 | if p.Readme != nil { | |
| 37 | - | <h2 class="rule-h"><span>{ p.Readme.Name }</span></h2> | |
| 38 | - | @ReadmeView(*p.Readme) | |
| 37 | + | @ReadmeCard(*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 | 7 | // View models handed to the templ components. Components only loop and | |
| 8 | 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 | 11 | type Crumb struct { | |
| 12 | 12 | Label string | |
| 13 | 13 | Href string | |
| 14 | 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 | 18 | type Section struct { | |
| 19 | 19 | Key string // mnemonic key | |
| 20 | - | Post string // rest of the label after the key | |
| 20 | + | Label string | |
| 21 | 21 | Href string | |
| 22 | 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 | 25 | // RefItem is a branch or tag in the ref switcher. | |
| 36 | 26 | type RefItem struct { | |
| 37 | 27 | Name string | |
| @@ -50,10 +40,9 @@ type Layout struct { | |||
| 50 | 40 | SourceURL string | |
| 51 | 41 | Assets Assets | |
| 52 | 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 | 44 | Short string // short sha shown after the ref | |
| 55 | 45 | Sections []Section | |
| 56 | - | Status Status | |
| 57 | 46 | Repo string | |
| 58 | 47 | SHA string // full sha of the ref, for the permalink key | |
| 59 | 48 | RefPath string // the URL segment that `y` rewrites: the ref name | |
| @@ -61,9 +50,7 @@ type Layout struct { | |||
| 61 | 50 | Tags []RefItem | |
| 62 | 51 | Cmd []CmdItem | |
| 63 | 52 | HasRail bool | |
| 64 | - | RailSum string // the rail as one line when it folds above the content (< 1100px) | |
| 65 | - | Read bool // prose page: content capped at the reading measure | |
| 66 | - | // Live connects the status bar to the event stream (/-/events). | |
| 53 | + | // Live connects the page to the event stream (/-/events). | |
| 67 | 54 | Live bool | |
| 68 | 55 | // LiveURL is the stream the page follows, with its query. | |
| 69 | 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 | 197 | h := w.Header() | |
| 198 | 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 | 202 | // Public pages: the browser and any proxy revalidate each time. A page | |
| 200 | 203 | // rendered for the owner is private and never stored; a handler that set | |
| 201 | 204 | // its own Cache-Control keeps it. | |
| @@ -253,18 +256,16 @@ func (s *Server) base() Layout { | |||
| 253 | 256 | } | |
| 254 | 257 | ||
| 255 | 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 | 260 | l := s.base() | |
| 258 | 261 | l.Title = title + " · " + s.instance | |
| 259 | 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 | 263 | l.Cmd = []CmdItem{{Kind: "go", Label: "home", Href: "/"}} | |
| 263 | 264 | return l | |
| 264 | 265 | } | |
| 265 | 266 | ||
| 266 | 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 | 269 | return State(l, StatePage{ | |
| 269 | 270 | Code: "404", Title: "not found", | |
| 270 | 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 | 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 | 278 | return State(l, StatePage{ | |
| 278 | 279 | Code: "500", Title: "internal error", | |
| 279 | 280 | Lines: []string{ | |