fix(web): persistent live regions, scroll-aware diff TOC, ermite.js handlers
- live banners update children of a persistent role=status slot - an IntersectionObserver marks the diff file in view - live stream reloads once on 404/401 then stops reconnecting - server values reach evaluated attributes through data-* only - copy feedback updates aria-label and the hint region - header refs is a real link; JS-only controls hide without scripts - keyboard, filter and palette logic moves from hx-on blocks to ermite.js - one htmx:error listener surfaces handler errors
- commit
- 0e9351b3972ab03d7031cadebdb723e7604fbbf2
- author
- brice · · 2026-10-11 20:01 UTC
- parent
- 94354ea
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
61 files · +2013 -3079
M cmd/ermite/testdata/web/blob-binary.html +27 -73
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">main</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -229,7 +229,7 @@ | |||
| 229 | 229 | <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()"> | |
| 230 | 230 | <div class="panel-head"> | |
| 231 | 231 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 232 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 233 | 233 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 234 | 234 | <kbd>esc</kbd> | |
| 235 | 235 | </button> | |
| @@ -264,13 +264,7 @@ | |||
| 264 | 264 | <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 | 265 | <div class="panel-head"> | |
| 266 | 266 | <label for="cmd-q" class="sr">go to</label> | |
| 267 | - | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | |
| 268 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 269 | - | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | |
| 270 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 271 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 272 | - | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | |
| 273 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 267 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 274 | 268 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 275 | 269 | <kbd>esc</kbd> | |
| 276 | 270 | </button> | |
| @@ -341,67 +335,27 @@ | |||
| 341 | 335 | <kbd>esc</kbd> close</span> | |
| 342 | 336 | </p> | |
| 343 | 337 | </dialog> | |
| 344 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 345 | - | if (q('dialog[open]').count) return; | |
| 346 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 347 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 348 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 349 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 350 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 351 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 352 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 353 | - | })(1); | |
| 354 | - | k-k from:body -> (async (d) => { | |
| 355 | - | if (q('dialog[open]').count) return; | |
| 356 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 357 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 358 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 359 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 360 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 361 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 362 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 363 | - | })(-1); | |
| 364 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 365 | - | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | |
| 366 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 367 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 368 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 369 | - | 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()); | |
| 370 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 371 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 372 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 373 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 374 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 375 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 376 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 377 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 378 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 379 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 380 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 381 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 382 | - | k-next from:body -> { | |
| 383 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 384 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 385 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 386 | - | }; | |
| 387 | - | k-split from:body -> { | |
| 388 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 389 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 390 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 391 | - | }; | |
| 392 | - | k-unified from:body -> { | |
| 393 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 394 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 395 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 396 | - | }; | |
| 397 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 398 | - | k-permalink from:body -> { | |
| 399 | - | let b = document.body.dataset; | |
| 400 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 401 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 402 | - | navigator.clipboard?.writeText(location.href); | |
| 403 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 404 | - | }"> | |
| 338 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 339 | + | k-k from:body -> ermite.move(-1); | |
| 340 | + | k-top from:body -> ermite.edge(false); | |
| 341 | + | k-bottom from:body -> ermite.edge(true); | |
| 342 | + | k-open from:body -> ermite.open(); | |
| 343 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 344 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 345 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 346 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 347 | + | k-filter from:body -> ermite.focusFilter(); | |
| 348 | + | k-gh from:body -> ermite.go('.brand'); | |
| 349 | + | k-s from:body -> ermite.tab('s'); | |
| 350 | + | k-t from:body -> ermite.tab('t'); | |
| 351 | + | k-c from:body -> ermite.tab('c'); | |
| 352 | + | k-r from:body -> ermite.tab('r'); | |
| 353 | + | k-m from:body -> ermite.tab('m'); | |
| 354 | + | k-next from:body -> ermite.nextNote(); | |
| 355 | + | k-split from:body -> ermite.layout('split'); | |
| 356 | + | k-unified from:body -> ermite.layout('unified'); | |
| 357 | + | k-collapse from:body -> ermite.collapse(); | |
| 358 | + | k-permalink from:body -> ermite.permalink()"> | |
| 405 | 359 | </div> | |
| 406 | 360 | </body> | |
| 407 | 361 | </html> | |
M cmd/ermite/testdata/web/blob-code.html +27 -73
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">main</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -299,7 +299,7 @@ | |||
| 299 | 299 | <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()"> | |
| 300 | 300 | <div class="panel-head"> | |
| 301 | 301 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 302 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 303 | 303 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 304 | 304 | <kbd>esc</kbd> | |
| 305 | 305 | </button> | |
| @@ -334,13 +334,7 @@ | |||
| 334 | 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()"> | |
| 335 | 335 | <div class="panel-head"> | |
| 336 | 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(); | |
| 338 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 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(); | |
| 340 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 341 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 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' })) } | |
| 343 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 344 | 338 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 345 | 339 | <kbd>esc</kbd> | |
| 346 | 340 | </button> | |
| @@ -411,67 +405,27 @@ | |||
| 411 | 405 | <kbd>esc</kbd> close</span> | |
| 412 | 406 | </p> | |
| 413 | 407 | </dialog> | |
| 414 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 415 | - | if (q('dialog[open]').count) return; | |
| 416 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 417 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 418 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 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 = '' | |
| 423 | - | })(1); | |
| 424 | - | k-k from:body -> (async (d) => { | |
| 425 | - | if (q('dialog[open]').count) return; | |
| 426 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 427 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 428 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 429 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 430 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 431 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 432 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 433 | - | })(-1); | |
| 434 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 435 | - | 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)); | |
| 436 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 437 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 438 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 439 | - | 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()); | |
| 440 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 441 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 442 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 443 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 444 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 445 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 446 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 447 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 448 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 449 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 450 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 451 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 452 | - | k-next from:body -> { | |
| 453 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 454 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 455 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 456 | - | }; | |
| 457 | - | k-split from:body -> { | |
| 458 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 459 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 460 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 461 | - | }; | |
| 462 | - | k-unified from:body -> { | |
| 463 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 464 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 465 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 466 | - | }; | |
| 467 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 468 | - | k-permalink from:body -> { | |
| 469 | - | let b = document.body.dataset; | |
| 470 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 471 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 472 | - | navigator.clipboard?.writeText(location.href); | |
| 473 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 474 | - | }"> | |
| 408 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 409 | + | k-k from:body -> ermite.move(-1); | |
| 410 | + | k-top from:body -> ermite.edge(false); | |
| 411 | + | k-bottom from:body -> ermite.edge(true); | |
| 412 | + | k-open from:body -> ermite.open(); | |
| 413 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 414 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 415 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 416 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 417 | + | k-filter from:body -> ermite.focusFilter(); | |
| 418 | + | k-gh from:body -> ermite.go('.brand'); | |
| 419 | + | k-s from:body -> ermite.tab('s'); | |
| 420 | + | k-t from:body -> ermite.tab('t'); | |
| 421 | + | k-c from:body -> ermite.tab('c'); | |
| 422 | + | k-r from:body -> ermite.tab('r'); | |
| 423 | + | k-m from:body -> ermite.tab('m'); | |
| 424 | + | k-next from:body -> ermite.nextNote(); | |
| 425 | + | k-split from:body -> ermite.layout('split'); | |
| 426 | + | k-unified from:body -> ermite.layout('unified'); | |
| 427 | + | k-collapse from:body -> ermite.collapse(); | |
| 428 | + | k-permalink from:body -> ermite.permalink()"> | |
| 475 | 429 | </div> | |
| 476 | 430 | </body> | |
| 477 | 431 | </html> | |
M cmd/ermite/testdata/web/blob-empty.html +27 -73
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">main</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -229,7 +229,7 @@ | |||
| 229 | 229 | <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()"> | |
| 230 | 230 | <div class="panel-head"> | |
| 231 | 231 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 232 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 233 | 233 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 234 | 234 | <kbd>esc</kbd> | |
| 235 | 235 | </button> | |
| @@ -264,13 +264,7 @@ | |||
| 264 | 264 | <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 | 265 | <div class="panel-head"> | |
| 266 | 266 | <label for="cmd-q" class="sr">go to</label> | |
| 267 | - | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | |
| 268 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 269 | - | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | |
| 270 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 271 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 272 | - | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | |
| 273 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 267 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 274 | 268 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 275 | 269 | <kbd>esc</kbd> | |
| 276 | 270 | </button> | |
| @@ -341,67 +335,27 @@ | |||
| 341 | 335 | <kbd>esc</kbd> close</span> | |
| 342 | 336 | </p> | |
| 343 | 337 | </dialog> | |
| 344 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 345 | - | if (q('dialog[open]').count) return; | |
| 346 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 347 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 348 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 349 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 350 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 351 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 352 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 353 | - | })(1); | |
| 354 | - | k-k from:body -> (async (d) => { | |
| 355 | - | if (q('dialog[open]').count) return; | |
| 356 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 357 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 358 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 359 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 360 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 361 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 362 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 363 | - | })(-1); | |
| 364 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 365 | - | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | |
| 366 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 367 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 368 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 369 | - | 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()); | |
| 370 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 371 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 372 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 373 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 374 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 375 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 376 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 377 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 378 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 379 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 380 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 381 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 382 | - | k-next from:body -> { | |
| 383 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 384 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 385 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 386 | - | }; | |
| 387 | - | k-split from:body -> { | |
| 388 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 389 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 390 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 391 | - | }; | |
| 392 | - | k-unified from:body -> { | |
| 393 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 394 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 395 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 396 | - | }; | |
| 397 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 398 | - | k-permalink from:body -> { | |
| 399 | - | let b = document.body.dataset; | |
| 400 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 401 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 402 | - | navigator.clipboard?.writeText(location.href); | |
| 403 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 404 | - | }"> | |
| 338 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 339 | + | k-k from:body -> ermite.move(-1); | |
| 340 | + | k-top from:body -> ermite.edge(false); | |
| 341 | + | k-bottom from:body -> ermite.edge(true); | |
| 342 | + | k-open from:body -> ermite.open(); | |
| 343 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 344 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 345 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 346 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 347 | + | k-filter from:body -> ermite.focusFilter(); | |
| 348 | + | k-gh from:body -> ermite.go('.brand'); | |
| 349 | + | k-s from:body -> ermite.tab('s'); | |
| 350 | + | k-t from:body -> ermite.tab('t'); | |
| 351 | + | k-c from:body -> ermite.tab('c'); | |
| 352 | + | k-r from:body -> ermite.tab('r'); | |
| 353 | + | k-m from:body -> ermite.tab('m'); | |
| 354 | + | k-next from:body -> ermite.nextNote(); | |
| 355 | + | k-split from:body -> ermite.layout('split'); | |
| 356 | + | k-unified from:body -> ermite.layout('unified'); | |
| 357 | + | k-collapse from:body -> ermite.collapse(); | |
| 358 | + | k-permalink from:body -> ermite.permalink()"> | |
| 405 | 359 | </div> | |
| 406 | 360 | </body> | |
| 407 | 361 | </html> | |
M cmd/ermite/testdata/web/blob-image.html +27 -73
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">main</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -231,7 +231,7 @@ | |||
| 231 | 231 | <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 | 232 | <div class="panel-head"> | |
| 233 | 233 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 234 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 235 | 235 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 236 | 236 | <kbd>esc</kbd> | |
| 237 | 237 | </button> | |
| @@ -266,13 +266,7 @@ | |||
| 266 | 266 | <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()"> | |
| 267 | 267 | <div class="panel-head"> | |
| 268 | 268 | <label for="cmd-q" class="sr">go to</label> | |
| 269 | - | <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(); | |
| 270 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 271 | - | 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(); | |
| 272 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 273 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 274 | - | 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' })) } | |
| 275 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 269 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 276 | 270 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 277 | 271 | <kbd>esc</kbd> | |
| 278 | 272 | </button> | |
| @@ -343,67 +337,27 @@ | |||
| 343 | 337 | <kbd>esc</kbd> close</span> | |
| 344 | 338 | </p> | |
| 345 | 339 | </dialog> | |
| 346 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 347 | - | if (q('dialog[open]').count) return; | |
| 348 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 349 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 350 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 351 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 352 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 353 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 354 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 355 | - | })(1); | |
| 356 | - | k-k from:body -> (async (d) => { | |
| 357 | - | if (q('dialog[open]').count) return; | |
| 358 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 359 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 360 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 361 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 362 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 363 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 364 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 365 | - | })(-1); | |
| 366 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 367 | - | 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)); | |
| 368 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 369 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 370 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 371 | - | 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()); | |
| 372 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 373 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 374 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 375 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 376 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 377 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 378 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 379 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 380 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 381 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 382 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 383 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 384 | - | k-next from:body -> { | |
| 385 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 386 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 387 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 388 | - | }; | |
| 389 | - | k-split from:body -> { | |
| 390 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 391 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 392 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 393 | - | }; | |
| 394 | - | k-unified from:body -> { | |
| 395 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 396 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 397 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 398 | - | }; | |
| 399 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 400 | - | k-permalink from:body -> { | |
| 401 | - | let b = document.body.dataset; | |
| 402 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 403 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 404 | - | navigator.clipboard?.writeText(location.href); | |
| 405 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 406 | - | }"> | |
| 340 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 341 | + | k-k from:body -> ermite.move(-1); | |
| 342 | + | k-top from:body -> ermite.edge(false); | |
| 343 | + | k-bottom from:body -> ermite.edge(true); | |
| 344 | + | k-open from:body -> ermite.open(); | |
| 345 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 346 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 347 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 348 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 349 | + | k-filter from:body -> ermite.focusFilter(); | |
| 350 | + | k-gh from:body -> ermite.go('.brand'); | |
| 351 | + | k-s from:body -> ermite.tab('s'); | |
| 352 | + | k-t from:body -> ermite.tab('t'); | |
| 353 | + | k-c from:body -> ermite.tab('c'); | |
| 354 | + | k-r from:body -> ermite.tab('r'); | |
| 355 | + | k-m from:body -> ermite.tab('m'); | |
| 356 | + | k-next from:body -> ermite.nextNote(); | |
| 357 | + | k-split from:body -> ermite.layout('split'); | |
| 358 | + | k-unified from:body -> ermite.layout('unified'); | |
| 359 | + | k-collapse from:body -> ermite.collapse(); | |
| 360 | + | k-permalink from:body -> ermite.permalink()"> | |
| 407 | 361 | </div> | |
| 408 | 362 | </body> | |
| 409 | 363 | </html> | |
M cmd/ermite/testdata/web/blob-markdown.html +27 -73
| @@ -47,19 +47,19 @@ | |||
| 47 | 47 | </ol> | |
| 48 | 48 | </nav> | |
| 49 | 49 | <div class="hdr-acts"> | |
| 50 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 51 | 51 | <span class="ico i-branch" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="ref-name">main</span> | |
| 54 | 54 | <span class="sha">SHA7</span> | |
| 55 | - | </button> | |
| 56 | - | <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'))"> | |
| 55 | + | </a> | |
| 56 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 57 | 57 | <span class="ico i-search" aria-hidden="true"> | |
| 58 | 58 | </span> | |
| 59 | 59 | <span class="search-label">Go to…</span> | |
| 60 | 60 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 61 | 61 | </button> | |
| 62 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 62 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 63 | 63 | </div> | |
| 64 | 64 | </div> | |
| 65 | 65 | <nav class="tabs" aria-label="Sections"> | |
| @@ -296,7 +296,7 @@ | |||
| 296 | 296 | <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()"> | |
| 297 | 297 | <div class="panel-head"> | |
| 298 | 298 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 299 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 300 | 300 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 301 | 301 | <kbd>esc</kbd> | |
| 302 | 302 | </button> | |
| @@ -331,13 +331,7 @@ | |||
| 331 | 331 | <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()"> | |
| 332 | 332 | <div class="panel-head"> | |
| 333 | 333 | <label for="cmd-q" class="sr">go to</label> | |
| 334 | - | <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(); | |
| 335 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 336 | - | 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(); | |
| 337 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 338 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 339 | - | 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' })) } | |
| 340 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 334 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 341 | 335 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 342 | 336 | <kbd>esc</kbd> | |
| 343 | 337 | </button> | |
| @@ -408,67 +402,27 @@ | |||
| 408 | 402 | <kbd>esc</kbd> close</span> | |
| 409 | 403 | </p> | |
| 410 | 404 | </dialog> | |
| 411 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 412 | - | if (q('dialog[open]').count) return; | |
| 413 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 414 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 415 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 416 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 417 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 418 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 419 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 420 | - | })(1); | |
| 421 | - | k-k from:body -> (async (d) => { | |
| 422 | - | if (q('dialog[open]').count) return; | |
| 423 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 424 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 425 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 426 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 427 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 428 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 429 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 430 | - | })(-1); | |
| 431 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 432 | - | 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)); | |
| 433 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 434 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 435 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 436 | - | 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()); | |
| 437 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 438 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 439 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 440 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 441 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 442 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 443 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 444 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 445 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 446 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 447 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 448 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 449 | - | k-next from:body -> { | |
| 450 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 451 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 452 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 453 | - | }; | |
| 454 | - | k-split from:body -> { | |
| 455 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 456 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 457 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 458 | - | }; | |
| 459 | - | k-unified from:body -> { | |
| 460 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 461 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 462 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 463 | - | }; | |
| 464 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 465 | - | k-permalink from:body -> { | |
| 466 | - | let b = document.body.dataset; | |
| 467 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 468 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 469 | - | navigator.clipboard?.writeText(location.href); | |
| 470 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 471 | - | }"> | |
| 405 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 406 | + | k-k from:body -> ermite.move(-1); | |
| 407 | + | k-top from:body -> ermite.edge(false); | |
| 408 | + | k-bottom from:body -> ermite.edge(true); | |
| 409 | + | k-open from:body -> ermite.open(); | |
| 410 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 411 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 412 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 413 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 414 | + | k-filter from:body -> ermite.focusFilter(); | |
| 415 | + | k-gh from:body -> ermite.go('.brand'); | |
| 416 | + | k-s from:body -> ermite.tab('s'); | |
| 417 | + | k-t from:body -> ermite.tab('t'); | |
| 418 | + | k-c from:body -> ermite.tab('c'); | |
| 419 | + | k-r from:body -> ermite.tab('r'); | |
| 420 | + | k-m from:body -> ermite.tab('m'); | |
| 421 | + | k-next from:body -> ermite.nextNote(); | |
| 422 | + | k-split from:body -> ermite.layout('split'); | |
| 423 | + | k-unified from:body -> ermite.layout('unified'); | |
| 424 | + | k-collapse from:body -> ermite.collapse(); | |
| 425 | + | k-permalink from:body -> ermite.permalink()"> | |
| 472 | 426 | </div> | |
| 473 | 427 | </body> | |
| 474 | 428 | </html> | |
M cmd/ermite/testdata/web/blob-svg.html +27 -73
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">main</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -254,7 +254,7 @@ | |||
| 254 | 254 | <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()"> | |
| 255 | 255 | <div class="panel-head"> | |
| 256 | 256 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 257 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 258 | 258 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 259 | 259 | <kbd>esc</kbd> | |
| 260 | 260 | </button> | |
| @@ -289,13 +289,7 @@ | |||
| 289 | 289 | <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()"> | |
| 290 | 290 | <div class="panel-head"> | |
| 291 | 291 | <label for="cmd-q" class="sr">go to</label> | |
| 292 | - | <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(); | |
| 293 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 294 | - | 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(); | |
| 295 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 296 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 297 | - | 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' })) } | |
| 298 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 292 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 299 | 293 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 300 | 294 | <kbd>esc</kbd> | |
| 301 | 295 | </button> | |
| @@ -366,67 +360,27 @@ | |||
| 366 | 360 | <kbd>esc</kbd> close</span> | |
| 367 | 361 | </p> | |
| 368 | 362 | </dialog> | |
| 369 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 370 | - | if (q('dialog[open]').count) return; | |
| 371 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 372 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 373 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 374 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 375 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 376 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 377 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 378 | - | })(1); | |
| 379 | - | k-k from:body -> (async (d) => { | |
| 380 | - | if (q('dialog[open]').count) return; | |
| 381 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 382 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 383 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 384 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 385 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 386 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 387 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 388 | - | })(-1); | |
| 389 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 390 | - | 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)); | |
| 391 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 392 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 393 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 394 | - | 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()); | |
| 395 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 396 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 397 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 398 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 399 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 400 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 401 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 402 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 403 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 404 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 405 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 406 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 407 | - | k-next from:body -> { | |
| 408 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 409 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 410 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 411 | - | }; | |
| 412 | - | k-split from:body -> { | |
| 413 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 414 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 415 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 416 | - | }; | |
| 417 | - | k-unified from:body -> { | |
| 418 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 419 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 420 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 421 | - | }; | |
| 422 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 423 | - | k-permalink from:body -> { | |
| 424 | - | let b = document.body.dataset; | |
| 425 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 426 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 427 | - | navigator.clipboard?.writeText(location.href); | |
| 428 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 429 | - | }"> | |
| 363 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 364 | + | k-k from:body -> ermite.move(-1); | |
| 365 | + | k-top from:body -> ermite.edge(false); | |
| 366 | + | k-bottom from:body -> ermite.edge(true); | |
| 367 | + | k-open from:body -> ermite.open(); | |
| 368 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 369 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 370 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 371 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 372 | + | k-filter from:body -> ermite.focusFilter(); | |
| 373 | + | k-gh from:body -> ermite.go('.brand'); | |
| 374 | + | k-s from:body -> ermite.tab('s'); | |
| 375 | + | k-t from:body -> ermite.tab('t'); | |
| 376 | + | k-c from:body -> ermite.tab('c'); | |
| 377 | + | k-r from:body -> ermite.tab('r'); | |
| 378 | + | k-m from:body -> ermite.tab('m'); | |
| 379 | + | k-next from:body -> ermite.nextNote(); | |
| 380 | + | k-split from:body -> ermite.layout('split'); | |
| 381 | + | k-unified from:body -> ermite.layout('unified'); | |
| 382 | + | k-collapse from:body -> ermite.collapse(); | |
| 383 | + | k-permalink from:body -> ermite.permalink()"> | |
| 430 | 384 | </div> | |
| 431 | 385 | </body> | |
| 432 | 386 | </html> | |
M cmd/ermite/testdata/web/blob-text.html +27 -73
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">main</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -249,7 +249,7 @@ | |||
| 249 | 249 | <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()"> | |
| 250 | 250 | <div class="panel-head"> | |
| 251 | 251 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 252 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 253 | 253 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 254 | 254 | <kbd>esc</kbd> | |
| 255 | 255 | </button> | |
| @@ -284,13 +284,7 @@ | |||
| 284 | 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()"> | |
| 285 | 285 | <div class="panel-head"> | |
| 286 | 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(); | |
| 288 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 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(); | |
| 290 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 291 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 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' })) } | |
| 293 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 294 | 288 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 295 | 289 | <kbd>esc</kbd> | |
| 296 | 290 | </button> | |
| @@ -361,67 +355,27 @@ | |||
| 361 | 355 | <kbd>esc</kbd> close</span> | |
| 362 | 356 | </p> | |
| 363 | 357 | </dialog> | |
| 364 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 365 | - | if (q('dialog[open]').count) return; | |
| 366 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 367 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 368 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 369 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 370 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 371 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 372 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 373 | - | })(1); | |
| 374 | - | k-k from:body -> (async (d) => { | |
| 375 | - | if (q('dialog[open]').count) return; | |
| 376 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 377 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 378 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 379 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 380 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 381 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 382 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 383 | - | })(-1); | |
| 384 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 385 | - | 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)); | |
| 386 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 387 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 388 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 389 | - | 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()); | |
| 390 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 391 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 392 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 393 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 394 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 395 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 396 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 397 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 398 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 399 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 400 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 401 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 402 | - | k-next from:body -> { | |
| 403 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 404 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 405 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 406 | - | }; | |
| 407 | - | k-split from:body -> { | |
| 408 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 409 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 410 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 411 | - | }; | |
| 412 | - | k-unified from:body -> { | |
| 413 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 414 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 415 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 416 | - | }; | |
| 417 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 418 | - | k-permalink from:body -> { | |
| 419 | - | let b = document.body.dataset; | |
| 420 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 421 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 422 | - | navigator.clipboard?.writeText(location.href); | |
| 423 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 424 | - | }"> | |
| 358 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 359 | + | k-k from:body -> ermite.move(-1); | |
| 360 | + | k-top from:body -> ermite.edge(false); | |
| 361 | + | k-bottom from:body -> ermite.edge(true); | |
| 362 | + | k-open from:body -> ermite.open(); | |
| 363 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 364 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 365 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 366 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 367 | + | k-filter from:body -> ermite.focusFilter(); | |
| 368 | + | k-gh from:body -> ermite.go('.brand'); | |
| 369 | + | k-s from:body -> ermite.tab('s'); | |
| 370 | + | k-t from:body -> ermite.tab('t'); | |
| 371 | + | k-c from:body -> ermite.tab('c'); | |
| 372 | + | k-r from:body -> ermite.tab('r'); | |
| 373 | + | k-m from:body -> ermite.tab('m'); | |
| 374 | + | k-next from:body -> ermite.nextNote(); | |
| 375 | + | k-split from:body -> ermite.layout('split'); | |
| 376 | + | k-unified from:body -> ermite.layout('unified'); | |
| 377 | + | k-collapse from:body -> ermite.collapse(); | |
| 378 | + | k-permalink from:body -> ermite.permalink()"> | |
| 425 | 379 | </div> | |
| 426 | 380 | </body> | |
| 427 | 381 | </html> | |
M cmd/ermite/testdata/web/blob-too-large.html +27 -73
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">main</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -229,7 +229,7 @@ | |||
| 229 | 229 | <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()"> | |
| 230 | 230 | <div class="panel-head"> | |
| 231 | 231 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 232 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 233 | 233 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 234 | 234 | <kbd>esc</kbd> | |
| 235 | 235 | </button> | |
| @@ -264,13 +264,7 @@ | |||
| 264 | 264 | <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 | 265 | <div class="panel-head"> | |
| 266 | 266 | <label for="cmd-q" class="sr">go to</label> | |
| 267 | - | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | |
| 268 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 269 | - | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | |
| 270 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 271 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 272 | - | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | |
| 273 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 267 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 274 | 268 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 275 | 269 | <kbd>esc</kbd> | |
| 276 | 270 | </button> | |
| @@ -341,67 +335,27 @@ | |||
| 341 | 335 | <kbd>esc</kbd> close</span> | |
| 342 | 336 | </p> | |
| 343 | 337 | </dialog> | |
| 344 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 345 | - | if (q('dialog[open]').count) return; | |
| 346 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 347 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 348 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 349 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 350 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 351 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 352 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 353 | - | })(1); | |
| 354 | - | k-k from:body -> (async (d) => { | |
| 355 | - | if (q('dialog[open]').count) return; | |
| 356 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 357 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 358 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 359 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 360 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 361 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 362 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 363 | - | })(-1); | |
| 364 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 365 | - | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | |
| 366 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 367 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 368 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 369 | - | 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()); | |
| 370 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 371 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 372 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 373 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 374 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 375 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 376 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 377 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 378 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 379 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 380 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 381 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 382 | - | k-next from:body -> { | |
| 383 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 384 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 385 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 386 | - | }; | |
| 387 | - | k-split from:body -> { | |
| 388 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 389 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 390 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 391 | - | }; | |
| 392 | - | k-unified from:body -> { | |
| 393 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 394 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 395 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 396 | - | }; | |
| 397 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 398 | - | k-permalink from:body -> { | |
| 399 | - | let b = document.body.dataset; | |
| 400 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 401 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 402 | - | navigator.clipboard?.writeText(location.href); | |
| 403 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 404 | - | }"> | |
| 338 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 339 | + | k-k from:body -> ermite.move(-1); | |
| 340 | + | k-top from:body -> ermite.edge(false); | |
| 341 | + | k-bottom from:body -> ermite.edge(true); | |
| 342 | + | k-open from:body -> ermite.open(); | |
| 343 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 344 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 345 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 346 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 347 | + | k-filter from:body -> ermite.focusFilter(); | |
| 348 | + | k-gh from:body -> ermite.go('.brand'); | |
| 349 | + | k-s from:body -> ermite.tab('s'); | |
| 350 | + | k-t from:body -> ermite.tab('t'); | |
| 351 | + | k-c from:body -> ermite.tab('c'); | |
| 352 | + | k-r from:body -> ermite.tab('r'); | |
| 353 | + | k-m from:body -> ermite.tab('m'); | |
| 354 | + | k-next from:body -> ermite.nextNote(); | |
| 355 | + | k-split from:body -> ermite.layout('split'); | |
| 356 | + | k-unified from:body -> ermite.layout('unified'); | |
| 357 | + | k-collapse from:body -> ermite.collapse(); | |
| 358 | + | k-permalink from:body -> ermite.permalink()"> | |
| 405 | 359 | </div> | |
| 406 | 360 | </body> | |
| 407 | 361 | </html> | |
M cmd/ermite/testdata/web/blob-trojan.html +27 -73
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">main</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -296,7 +296,7 @@ | |||
| 296 | 296 | <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()"> | |
| 297 | 297 | <div class="panel-head"> | |
| 298 | 298 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 299 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 300 | 300 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 301 | 301 | <kbd>esc</kbd> | |
| 302 | 302 | </button> | |
| @@ -331,13 +331,7 @@ | |||
| 331 | 331 | <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()"> | |
| 332 | 332 | <div class="panel-head"> | |
| 333 | 333 | <label for="cmd-q" class="sr">go to</label> | |
| 334 | - | <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(); | |
| 335 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 336 | - | 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(); | |
| 337 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 338 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 339 | - | 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' })) } | |
| 340 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 334 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 341 | 335 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 342 | 336 | <kbd>esc</kbd> | |
| 343 | 337 | </button> | |
| @@ -408,67 +402,27 @@ | |||
| 408 | 402 | <kbd>esc</kbd> close</span> | |
| 409 | 403 | </p> | |
| 410 | 404 | </dialog> | |
| 411 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 412 | - | if (q('dialog[open]').count) return; | |
| 413 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 414 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 415 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 416 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 417 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 418 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 419 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 420 | - | })(1); | |
| 421 | - | k-k from:body -> (async (d) => { | |
| 422 | - | if (q('dialog[open]').count) return; | |
| 423 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 424 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 425 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 426 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 427 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 428 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 429 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 430 | - | })(-1); | |
| 431 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 432 | - | 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)); | |
| 433 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 434 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 435 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 436 | - | 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()); | |
| 437 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 438 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 439 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 440 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 441 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 442 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 443 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 444 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 445 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 446 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 447 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 448 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 449 | - | k-next from:body -> { | |
| 450 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 451 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 452 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 453 | - | }; | |
| 454 | - | k-split from:body -> { | |
| 455 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 456 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 457 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 458 | - | }; | |
| 459 | - | k-unified from:body -> { | |
| 460 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 461 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 462 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 463 | - | }; | |
| 464 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 465 | - | k-permalink from:body -> { | |
| 466 | - | let b = document.body.dataset; | |
| 467 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 468 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 469 | - | navigator.clipboard?.writeText(location.href); | |
| 470 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 471 | - | }"> | |
| 405 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 406 | + | k-k from:body -> ermite.move(-1); | |
| 407 | + | k-top from:body -> ermite.edge(false); | |
| 408 | + | k-bottom from:body -> ermite.edge(true); | |
| 409 | + | k-open from:body -> ermite.open(); | |
| 410 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 411 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 412 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 413 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 414 | + | k-filter from:body -> ermite.focusFilter(); | |
| 415 | + | k-gh from:body -> ermite.go('.brand'); | |
| 416 | + | k-s from:body -> ermite.tab('s'); | |
| 417 | + | k-t from:body -> ermite.tab('t'); | |
| 418 | + | k-c from:body -> ermite.tab('c'); | |
| 419 | + | k-r from:body -> ermite.tab('r'); | |
| 420 | + | k-m from:body -> ermite.tab('m'); | |
| 421 | + | k-next from:body -> ermite.nextNote(); | |
| 422 | + | k-split from:body -> ermite.layout('split'); | |
| 423 | + | k-unified from:body -> ermite.layout('unified'); | |
| 424 | + | k-collapse from:body -> ermite.collapse(); | |
| 425 | + | k-permalink from:body -> ermite.permalink()"> | |
| 472 | 426 | </div> | |
| 473 | 427 | </body> | |
| 474 | 428 | </html> | |
M cmd/ermite/testdata/web/commit-big.html +31 -77
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/diffs/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">detached</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -106,11 +106,11 @@ | |||
| 106 | 106 | </span> | |
| 107 | 107 | <span class="grow"> | |
| 108 | 108 | </span> | |
| 109 | - | <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 aria-hidden="true">u</kbd> | |
| 109 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 110 | 110 | </a> | |
| 111 | - | <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 aria-hidden="true">d</kbd> | |
| 111 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 112 | 112 | </a> | |
| 113 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn"> | |
| 113 | + | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 114 | 114 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| 115 | 115 | <kbd aria-hidden="true">x</kbd> | |
| 116 | 116 | </button> | |
| @@ -135,7 +135,7 @@ | |||
| 135 | 135 | </h2> | |
| 136 | 136 | <ol class="files" aria-label="Changed files"> | |
| 137 | 137 | <li> | |
| 138 | - | <a href="#f1" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f1' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 138 | + | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 139 | 139 | <span class="st" title="A">A</span> | |
| 140 | 140 | <span class="p" title="big.txt">big.txt</span> | |
| 141 | 141 | <span class="n"> | |
| @@ -266,7 +266,7 @@ | |||
| 266 | 266 | <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()"> | |
| 267 | 267 | <div class="panel-head"> | |
| 268 | 268 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 269 | - | <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))"> | |
| 269 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 270 | 270 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 271 | 271 | <kbd>esc</kbd> | |
| 272 | 272 | </button> | |
| @@ -301,13 +301,7 @@ | |||
| 301 | 301 | <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 | 302 | <div class="panel-head"> | |
| 303 | 303 | <label for="cmd-q" class="sr">go to</label> | |
| 304 | - | <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="let t = this.value.toLowerCase().trim(); | |
| 305 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 306 | - | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | |
| 307 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 308 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 309 | - | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | |
| 310 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 304 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 311 | 305 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 312 | 306 | <kbd>esc</kbd> | |
| 313 | 307 | </button> | |
| @@ -378,67 +372,27 @@ | |||
| 378 | 372 | <kbd>esc</kbd> close</span> | |
| 379 | 373 | </p> | |
| 380 | 374 | </dialog> | |
| 381 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 382 | - | if (q('dialog[open]').count) return; | |
| 383 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 384 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 385 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 386 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 387 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 388 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 389 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 390 | - | })(1); | |
| 391 | - | k-k from:body -> (async (d) => { | |
| 392 | - | if (q('dialog[open]').count) return; | |
| 393 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 394 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 395 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 396 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 397 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 398 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 399 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 400 | - | })(-1); | |
| 401 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 402 | - | 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)); | |
| 403 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 404 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 405 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 406 | - | 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()); | |
| 407 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 408 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 409 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 410 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 411 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 412 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 413 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 414 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 415 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 416 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 417 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 418 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 419 | - | k-next from:body -> { | |
| 420 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 421 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 422 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 423 | - | }; | |
| 424 | - | k-split from:body -> { | |
| 425 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 426 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 427 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 428 | - | }; | |
| 429 | - | k-unified from:body -> { | |
| 430 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 431 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 432 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 433 | - | }; | |
| 434 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 435 | - | k-permalink from:body -> { | |
| 436 | - | let b = document.body.dataset; | |
| 437 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 438 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 439 | - | navigator.clipboard?.writeText(location.href); | |
| 440 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 441 | - | }"> | |
| 375 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 376 | + | k-k from:body -> ermite.move(-1); | |
| 377 | + | k-top from:body -> ermite.edge(false); | |
| 378 | + | k-bottom from:body -> ermite.edge(true); | |
| 379 | + | k-open from:body -> ermite.open(); | |
| 380 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 381 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 382 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 383 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 384 | + | k-filter from:body -> ermite.focusFilter(); | |
| 385 | + | k-gh from:body -> ermite.go('.brand'); | |
| 386 | + | k-s from:body -> ermite.tab('s'); | |
| 387 | + | k-t from:body -> ermite.tab('t'); | |
| 388 | + | k-c from:body -> ermite.tab('c'); | |
| 389 | + | k-r from:body -> ermite.tab('r'); | |
| 390 | + | k-m from:body -> ermite.tab('m'); | |
| 391 | + | k-next from:body -> ermite.nextNote(); | |
| 392 | + | k-split from:body -> ermite.layout('split'); | |
| 393 | + | k-unified from:body -> ermite.layout('unified'); | |
| 394 | + | k-collapse from:body -> ermite.collapse(); | |
| 395 | + | k-permalink from:body -> ermite.permalink()"> | |
| 442 | 396 | </div> | |
| 443 | 397 | </body> | |
| 444 | 398 | </html> | |
M cmd/ermite/testdata/web/commit-binary.html +31 -77
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/diffs/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">detached</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -102,11 +102,11 @@ | |||
| 102 | 102 | </span> | |
| 103 | 103 | <span class="grow"> | |
| 104 | 104 | </span> | |
| 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 aria-hidden="true">u</kbd> | |
| 105 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 106 | 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 aria-hidden="true">d</kbd> | |
| 107 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 108 | 108 | </a> | |
| 109 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn"> | |
| 109 | + | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 110 | 110 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| 111 | 111 | <kbd aria-hidden="true">x</kbd> | |
| 112 | 112 | </button> | |
| @@ -130,7 +130,7 @@ | |||
| 130 | 130 | </h2> | |
| 131 | 131 | <ol class="files" aria-label="Changed files"> | |
| 132 | 132 | <li> | |
| 133 | - | <a href="#f1" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f1' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 133 | + | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 134 | 134 | <span class="st" title="M">M</span> | |
| 135 | 135 | <span class="p" title="logo.bin">logo.bin</span> | |
| 136 | 136 | <span class="n"> | |
| @@ -261,7 +261,7 @@ | |||
| 261 | 261 | <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()"> | |
| 262 | 262 | <div class="panel-head"> | |
| 263 | 263 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 264 | - | <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))"> | |
| 264 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 265 | 265 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 266 | 266 | <kbd>esc</kbd> | |
| 267 | 267 | </button> | |
| @@ -296,13 +296,7 @@ | |||
| 296 | 296 | <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()"> | |
| 297 | 297 | <div class="panel-head"> | |
| 298 | 298 | <label for="cmd-q" class="sr">go to</label> | |
| 299 | - | <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(); | |
| 300 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 301 | - | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | |
| 302 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 303 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 304 | - | if (step) { event.preventDefault(); let n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; n && (q(n).take('data-cursor', '#cmd-list > li'), n.scrollIntoView({ block: 'nearest' })) } | |
| 305 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 299 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 306 | 300 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 307 | 301 | <kbd>esc</kbd> | |
| 308 | 302 | </button> | |
| @@ -373,67 +367,27 @@ | |||
| 373 | 367 | <kbd>esc</kbd> close</span> | |
| 374 | 368 | </p> | |
| 375 | 369 | </dialog> | |
| 376 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 377 | - | if (q('dialog[open]').count) return; | |
| 378 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 379 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 380 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 381 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 382 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 383 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 384 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 385 | - | })(1); | |
| 386 | - | k-k from:body -> (async (d) => { | |
| 387 | - | if (q('dialog[open]').count) return; | |
| 388 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 389 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 390 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 391 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 392 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 393 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 394 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 395 | - | })(-1); | |
| 396 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 397 | - | 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)); | |
| 398 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 399 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 400 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 401 | - | 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()); | |
| 402 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 403 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 404 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 405 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 406 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 407 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 408 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 409 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 410 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 411 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 412 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 413 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 414 | - | k-next from:body -> { | |
| 415 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 416 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 417 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 418 | - | }; | |
| 419 | - | k-split from:body -> { | |
| 420 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 421 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 422 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 423 | - | }; | |
| 424 | - | k-unified from:body -> { | |
| 425 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 426 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 427 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 428 | - | }; | |
| 429 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 430 | - | k-permalink from:body -> { | |
| 431 | - | let b = document.body.dataset; | |
| 432 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 433 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 434 | - | navigator.clipboard?.writeText(location.href); | |
| 435 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 436 | - | }"> | |
| 370 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 371 | + | k-k from:body -> ermite.move(-1); | |
| 372 | + | k-top from:body -> ermite.edge(false); | |
| 373 | + | k-bottom from:body -> ermite.edge(true); | |
| 374 | + | k-open from:body -> ermite.open(); | |
| 375 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 376 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 377 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 378 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 379 | + | k-filter from:body -> ermite.focusFilter(); | |
| 380 | + | k-gh from:body -> ermite.go('.brand'); | |
| 381 | + | k-s from:body -> ermite.tab('s'); | |
| 382 | + | k-t from:body -> ermite.tab('t'); | |
| 383 | + | k-c from:body -> ermite.tab('c'); | |
| 384 | + | k-r from:body -> ermite.tab('r'); | |
| 385 | + | k-m from:body -> ermite.tab('m'); | |
| 386 | + | k-next from:body -> ermite.nextNote(); | |
| 387 | + | k-split from:body -> ermite.layout('split'); | |
| 388 | + | k-unified from:body -> ermite.layout('unified'); | |
| 389 | + | k-collapse from:body -> ermite.collapse(); | |
| 390 | + | k-permalink from:body -> ermite.permalink()"> | |
| 437 | 391 | </div> | |
| 438 | 392 | </body> | |
| 439 | 393 | </html> | |
M cmd/ermite/testdata/web/commit-merge.html +31 -77
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/diffs/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">detached</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -101,11 +101,11 @@ | |||
| 101 | 101 | </span> | |
| 102 | 102 | <span class="grow"> | |
| 103 | 103 | </span> | |
| 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 aria-hidden="true">u</kbd> | |
| 104 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 105 | 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 aria-hidden="true">d</kbd> | |
| 106 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 107 | 107 | </a> | |
| 108 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn"> | |
| 108 | + | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 109 | 109 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| 110 | 110 | <kbd aria-hidden="true">x</kbd> | |
| 111 | 111 | </button> | |
| @@ -169,7 +169,7 @@ | |||
| 169 | 169 | </h2> | |
| 170 | 170 | <ol class="files" aria-label="Changed files"> | |
| 171 | 171 | <li> | |
| 172 | - | <a href="#f1" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f1' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 172 | + | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 173 | 173 | <span class="st" title="A">A</span> | |
| 174 | 174 | <span class="p" title="side.txt">side.txt</span> | |
| 175 | 175 | <span class="n"> | |
| @@ -300,7 +300,7 @@ | |||
| 300 | 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()"> | |
| 301 | 301 | <div class="panel-head"> | |
| 302 | 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))"> | |
| 303 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 304 | 304 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 305 | 305 | <kbd>esc</kbd> | |
| 306 | 306 | </button> | |
| @@ -335,13 +335,7 @@ | |||
| 335 | 335 | <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()"> | |
| 336 | 336 | <div class="panel-head"> | |
| 337 | 337 | <label for="cmd-q" class="sr">go to</label> | |
| 338 | - | <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(); | |
| 339 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 340 | - | 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(); | |
| 341 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 342 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 343 | - | 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' })) } | |
| 344 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 338 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 345 | 339 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 346 | 340 | <kbd>esc</kbd> | |
| 347 | 341 | </button> | |
| @@ -412,67 +406,27 @@ | |||
| 412 | 406 | <kbd>esc</kbd> close</span> | |
| 413 | 407 | </p> | |
| 414 | 408 | </dialog> | |
| 415 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 416 | - | if (q('dialog[open]').count) return; | |
| 417 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 418 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 419 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 420 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 421 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 422 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 423 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 424 | - | })(1); | |
| 425 | - | k-k from:body -> (async (d) => { | |
| 426 | - | if (q('dialog[open]').count) return; | |
| 427 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 428 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 429 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 430 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 431 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 432 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 433 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 434 | - | })(-1); | |
| 435 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 436 | - | 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)); | |
| 437 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 438 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 439 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 440 | - | 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()); | |
| 441 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 442 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 443 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 444 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 445 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 446 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 447 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 448 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 449 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 450 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 451 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 452 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 453 | - | k-next from:body -> { | |
| 454 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 455 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 456 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 457 | - | }; | |
| 458 | - | k-split from:body -> { | |
| 459 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 460 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 461 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 462 | - | }; | |
| 463 | - | k-unified from:body -> { | |
| 464 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 465 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 466 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 467 | - | }; | |
| 468 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 469 | - | k-permalink from:body -> { | |
| 470 | - | let b = document.body.dataset; | |
| 471 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 472 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 473 | - | navigator.clipboard?.writeText(location.href); | |
| 474 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 475 | - | }"> | |
| 409 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 410 | + | k-k from:body -> ermite.move(-1); | |
| 411 | + | k-top from:body -> ermite.edge(false); | |
| 412 | + | k-bottom from:body -> ermite.edge(true); | |
| 413 | + | k-open from:body -> ermite.open(); | |
| 414 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 415 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 416 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 417 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 418 | + | k-filter from:body -> ermite.focusFilter(); | |
| 419 | + | k-gh from:body -> ermite.go('.brand'); | |
| 420 | + | k-s from:body -> ermite.tab('s'); | |
| 421 | + | k-t from:body -> ermite.tab('t'); | |
| 422 | + | k-c from:body -> ermite.tab('c'); | |
| 423 | + | k-r from:body -> ermite.tab('r'); | |
| 424 | + | k-m from:body -> ermite.tab('m'); | |
| 425 | + | k-next from:body -> ermite.nextNote(); | |
| 426 | + | k-split from:body -> ermite.layout('split'); | |
| 427 | + | k-unified from:body -> ermite.layout('unified'); | |
| 428 | + | k-collapse from:body -> ermite.collapse(); | |
| 429 | + | k-permalink from:body -> ermite.permalink()"> | |
| 476 | 430 | </div> | |
| 477 | 431 | </body> | |
| 478 | 432 | </html> | |
M cmd/ermite/testdata/web/commit-modify-split.html +32 -78
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/diffs/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">detached</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -108,11 +108,11 @@ And <b>html</b> & "quotes".</pre> | |||
| 108 | 108 | </span> | |
| 109 | 109 | <span class="grow"> | |
| 110 | 110 | </span> | |
| 111 | - | <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 aria-hidden="true">u</kbd> | |
| 111 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 112 | 112 | </a> | |
| 113 | - | <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 aria-hidden="true">d</kbd> | |
| 113 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" aria-current="true" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 114 | 114 | </a> | |
| 115 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn"> | |
| 115 | + | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 116 | 116 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| 117 | 117 | <kbd aria-hidden="true">x</kbd> | |
| 118 | 118 | </button> | |
| @@ -276,7 +276,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 276 | 276 | </h2> | |
| 277 | 277 | <ol class="files" aria-label="Changed files"> | |
| 278 | 278 | <li> | |
| 279 | - | <a href="#f1" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f1' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 279 | + | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 280 | 280 | <span class="st" title="M">M</span> | |
| 281 | 281 | <span class="p" title="a.txt">a.txt</span> | |
| 282 | 282 | <span class="n"> | |
| @@ -290,7 +290,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 290 | 290 | </a> | |
| 291 | 291 | </li> | |
| 292 | 292 | <li> | |
| 293 | - | <a href="#f2" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f2' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 293 | + | <a href="#f2" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 294 | 294 | <span class="st" title="M">M</span> | |
| 295 | 295 | <span class="p" title="run.sh">run.sh</span> | |
| 296 | 296 | <span class="n"> | |
| @@ -421,7 +421,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 421 | 421 | <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()"> | |
| 422 | 422 | <div class="panel-head"> | |
| 423 | 423 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 424 | - | <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))"> | |
| 424 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 425 | 425 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 426 | 426 | <kbd>esc</kbd> | |
| 427 | 427 | </button> | |
| @@ -456,13 +456,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 456 | 456 | <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()"> | |
| 457 | 457 | <div class="panel-head"> | |
| 458 | 458 | <label for="cmd-q" class="sr">go to</label> | |
| 459 | - | <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(); | |
| 460 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 461 | - | 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(); | |
| 462 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 463 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 464 | - | 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' })) } | |
| 465 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 459 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 466 | 460 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 467 | 461 | <kbd>esc</kbd> | |
| 468 | 462 | </button> | |
| @@ -533,67 +527,27 @@ And <b>html</b> & "quotes".</pre> | |||
| 533 | 527 | <kbd>esc</kbd> close</span> | |
| 534 | 528 | </p> | |
| 535 | 529 | </dialog> | |
| 536 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 537 | - | if (q('dialog[open]').count) return; | |
| 538 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 539 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 540 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 541 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 542 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 543 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 544 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 545 | - | })(1); | |
| 546 | - | k-k from:body -> (async (d) => { | |
| 547 | - | if (q('dialog[open]').count) return; | |
| 548 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 549 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 550 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 551 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 552 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 553 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 554 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 555 | - | })(-1); | |
| 556 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 557 | - | 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)); | |
| 558 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 559 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 560 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 561 | - | 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()); | |
| 562 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 563 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 564 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 565 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 566 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 567 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 568 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 569 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 570 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 571 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 572 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 573 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 574 | - | k-next from:body -> { | |
| 575 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 576 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 577 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 578 | - | }; | |
| 579 | - | k-split from:body -> { | |
| 580 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 581 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 582 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 583 | - | }; | |
| 584 | - | k-unified from:body -> { | |
| 585 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 586 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 587 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 588 | - | }; | |
| 589 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 590 | - | k-permalink from:body -> { | |
| 591 | - | let b = document.body.dataset; | |
| 592 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 593 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 594 | - | navigator.clipboard?.writeText(location.href); | |
| 595 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 596 | - | }"> | |
| 530 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 531 | + | k-k from:body -> ermite.move(-1); | |
| 532 | + | k-top from:body -> ermite.edge(false); | |
| 533 | + | k-bottom from:body -> ermite.edge(true); | |
| 534 | + | k-open from:body -> ermite.open(); | |
| 535 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 536 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 537 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 538 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 539 | + | k-filter from:body -> ermite.focusFilter(); | |
| 540 | + | k-gh from:body -> ermite.go('.brand'); | |
| 541 | + | k-s from:body -> ermite.tab('s'); | |
| 542 | + | k-t from:body -> ermite.tab('t'); | |
| 543 | + | k-c from:body -> ermite.tab('c'); | |
| 544 | + | k-r from:body -> ermite.tab('r'); | |
| 545 | + | k-m from:body -> ermite.tab('m'); | |
| 546 | + | k-next from:body -> ermite.nextNote(); | |
| 547 | + | k-split from:body -> ermite.layout('split'); | |
| 548 | + | k-unified from:body -> ermite.layout('unified'); | |
| 549 | + | k-collapse from:body -> ermite.collapse(); | |
| 550 | + | k-permalink from:body -> ermite.permalink()"> | |
| 597 | 551 | </div> | |
| 598 | 552 | </body> | |
| 599 | 553 | </html> | |
M cmd/ermite/testdata/web/commit-modify.html +32 -78
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/diffs/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">detached</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -108,11 +108,11 @@ And <b>html</b> & "quotes".</pre> | |||
| 108 | 108 | </span> | |
| 109 | 109 | <span class="grow"> | |
| 110 | 110 | </span> | |
| 111 | - | <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 aria-hidden="true">u</kbd> | |
| 111 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 112 | 112 | </a> | |
| 113 | - | <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 aria-hidden="true">d</kbd> | |
| 113 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 114 | 114 | </a> | |
| 115 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn"> | |
| 115 | + | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 116 | 116 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| 117 | 117 | <kbd aria-hidden="true">x</kbd> | |
| 118 | 118 | </button> | |
| @@ -276,7 +276,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 276 | 276 | </h2> | |
| 277 | 277 | <ol class="files" aria-label="Changed files"> | |
| 278 | 278 | <li> | |
| 279 | - | <a href="#f1" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f1' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 279 | + | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 280 | 280 | <span class="st" title="M">M</span> | |
| 281 | 281 | <span class="p" title="a.txt">a.txt</span> | |
| 282 | 282 | <span class="n"> | |
| @@ -290,7 +290,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 290 | 290 | </a> | |
| 291 | 291 | </li> | |
| 292 | 292 | <li> | |
| 293 | - | <a href="#f2" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f2' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 293 | + | <a href="#f2" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 294 | 294 | <span class="st" title="M">M</span> | |
| 295 | 295 | <span class="p" title="run.sh">run.sh</span> | |
| 296 | 296 | <span class="n"> | |
| @@ -421,7 +421,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 421 | 421 | <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()"> | |
| 422 | 422 | <div class="panel-head"> | |
| 423 | 423 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 424 | - | <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))"> | |
| 424 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 425 | 425 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 426 | 426 | <kbd>esc</kbd> | |
| 427 | 427 | </button> | |
| @@ -456,13 +456,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 456 | 456 | <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()"> | |
| 457 | 457 | <div class="panel-head"> | |
| 458 | 458 | <label for="cmd-q" class="sr">go to</label> | |
| 459 | - | <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(); | |
| 460 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 461 | - | 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(); | |
| 462 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 463 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 464 | - | 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' })) } | |
| 465 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 459 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 466 | 460 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 467 | 461 | <kbd>esc</kbd> | |
| 468 | 462 | </button> | |
| @@ -533,67 +527,27 @@ And <b>html</b> & "quotes".</pre> | |||
| 533 | 527 | <kbd>esc</kbd> close</span> | |
| 534 | 528 | </p> | |
| 535 | 529 | </dialog> | |
| 536 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 537 | - | if (q('dialog[open]').count) return; | |
| 538 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 539 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 540 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 541 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 542 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 543 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 544 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 545 | - | })(1); | |
| 546 | - | k-k from:body -> (async (d) => { | |
| 547 | - | if (q('dialog[open]').count) return; | |
| 548 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 549 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 550 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 551 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 552 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 553 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 554 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 555 | - | })(-1); | |
| 556 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 557 | - | 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)); | |
| 558 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 559 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 560 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 561 | - | 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()); | |
| 562 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 563 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 564 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 565 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 566 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 567 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 568 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 569 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 570 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 571 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 572 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 573 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 574 | - | k-next from:body -> { | |
| 575 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 576 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 577 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 578 | - | }; | |
| 579 | - | k-split from:body -> { | |
| 580 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 581 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 582 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 583 | - | }; | |
| 584 | - | k-unified from:body -> { | |
| 585 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 586 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 587 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 588 | - | }; | |
| 589 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 590 | - | k-permalink from:body -> { | |
| 591 | - | let b = document.body.dataset; | |
| 592 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 593 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 594 | - | navigator.clipboard?.writeText(location.href); | |
| 595 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 596 | - | }"> | |
| 530 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 531 | + | k-k from:body -> ermite.move(-1); | |
| 532 | + | k-top from:body -> ermite.edge(false); | |
| 533 | + | k-bottom from:body -> ermite.edge(true); | |
| 534 | + | k-open from:body -> ermite.open(); | |
| 535 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 536 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 537 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 538 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 539 | + | k-filter from:body -> ermite.focusFilter(); | |
| 540 | + | k-gh from:body -> ermite.go('.brand'); | |
| 541 | + | k-s from:body -> ermite.tab('s'); | |
| 542 | + | k-t from:body -> ermite.tab('t'); | |
| 543 | + | k-c from:body -> ermite.tab('c'); | |
| 544 | + | k-r from:body -> ermite.tab('r'); | |
| 545 | + | k-m from:body -> ermite.tab('m'); | |
| 546 | + | k-next from:body -> ermite.nextNote(); | |
| 547 | + | k-split from:body -> ermite.layout('split'); | |
| 548 | + | k-unified from:body -> ermite.layout('unified'); | |
| 549 | + | k-collapse from:body -> ermite.collapse(); | |
| 550 | + | k-permalink from:body -> ermite.permalink()"> | |
| 597 | 551 | </div> | |
| 598 | 552 | </body> | |
| 599 | 553 | </html> | |
M cmd/ermite/testdata/web/commit-rename.html +32 -78
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/diffs/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">detached</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -102,11 +102,11 @@ | |||
| 102 | 102 | </span> | |
| 103 | 103 | <span class="grow"> | |
| 104 | 104 | </span> | |
| 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 aria-hidden="true">u</kbd> | |
| 105 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 106 | 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 aria-hidden="true">d</kbd> | |
| 107 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 108 | 108 | </a> | |
| 109 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn"> | |
| 109 | + | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 110 | 110 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| 111 | 111 | <kbd aria-hidden="true">x</kbd> | |
| 112 | 112 | </button> | |
| @@ -180,7 +180,7 @@ | |||
| 180 | 180 | </h2> | |
| 181 | 181 | <ol class="files" aria-label="Changed files"> | |
| 182 | 182 | <li> | |
| 183 | - | <a href="#f1" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f1' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 183 | + | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 184 | 184 | <span class="st" title="R">R</span> | |
| 185 | 185 | <span class="p" title="old.txt → docs-old.txt">old.txt → docs-old.txt</span> | |
| 186 | 186 | <span class="n"> | |
| @@ -195,7 +195,7 @@ | |||
| 195 | 195 | </a> | |
| 196 | 196 | </li> | |
| 197 | 197 | <li> | |
| 198 | - | <a href="#f2" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f2' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 198 | + | <a href="#f2" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 199 | 199 | <span class="st" title="D">D</span> | |
| 200 | 200 | <span class="p" title="gone.txt">gone.txt</span> | |
| 201 | 201 | <span class="n"> | |
| @@ -326,7 +326,7 @@ | |||
| 326 | 326 | <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()"> | |
| 327 | 327 | <div class="panel-head"> | |
| 328 | 328 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 329 | - | <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))"> | |
| 329 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 330 | 330 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 331 | 331 | <kbd>esc</kbd> | |
| 332 | 332 | </button> | |
| @@ -361,13 +361,7 @@ | |||
| 361 | 361 | <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()"> | |
| 362 | 362 | <div class="panel-head"> | |
| 363 | 363 | <label for="cmd-q" class="sr">go to</label> | |
| 364 | - | <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(); | |
| 365 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 366 | - | 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(); | |
| 367 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 368 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 369 | - | 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' })) } | |
| 370 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 364 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 371 | 365 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 372 | 366 | <kbd>esc</kbd> | |
| 373 | 367 | </button> | |
| @@ -438,67 +432,27 @@ | |||
| 438 | 432 | <kbd>esc</kbd> close</span> | |
| 439 | 433 | </p> | |
| 440 | 434 | </dialog> | |
| 441 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 442 | - | if (q('dialog[open]').count) return; | |
| 443 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 444 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 445 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 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 = '' | |
| 450 | - | })(1); | |
| 451 | - | k-k from:body -> (async (d) => { | |
| 452 | - | if (q('dialog[open]').count) return; | |
| 453 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 454 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 455 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 456 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 457 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 458 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 459 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 460 | - | })(-1); | |
| 461 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 462 | - | 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)); | |
| 463 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 464 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 465 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 466 | - | 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()); | |
| 467 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 468 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 469 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 470 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 471 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 472 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 473 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 474 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 475 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 476 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 477 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 478 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 479 | - | k-next from:body -> { | |
| 480 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 481 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 482 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 483 | - | }; | |
| 484 | - | k-split from:body -> { | |
| 485 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 486 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 487 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 488 | - | }; | |
| 489 | - | k-unified from:body -> { | |
| 490 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 491 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 492 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 493 | - | }; | |
| 494 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 495 | - | k-permalink from:body -> { | |
| 496 | - | let b = document.body.dataset; | |
| 497 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 498 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 499 | - | navigator.clipboard?.writeText(location.href); | |
| 500 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 501 | - | }"> | |
| 435 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 436 | + | k-k from:body -> ermite.move(-1); | |
| 437 | + | k-top from:body -> ermite.edge(false); | |
| 438 | + | k-bottom from:body -> ermite.edge(true); | |
| 439 | + | k-open from:body -> ermite.open(); | |
| 440 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 441 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 442 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 443 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 444 | + | k-filter from:body -> ermite.focusFilter(); | |
| 445 | + | k-gh from:body -> ermite.go('.brand'); | |
| 446 | + | k-s from:body -> ermite.tab('s'); | |
| 447 | + | k-t from:body -> ermite.tab('t'); | |
| 448 | + | k-c from:body -> ermite.tab('c'); | |
| 449 | + | k-r from:body -> ermite.tab('r'); | |
| 450 | + | k-m from:body -> ermite.tab('m'); | |
| 451 | + | k-next from:body -> ermite.nextNote(); | |
| 452 | + | k-split from:body -> ermite.layout('split'); | |
| 453 | + | k-unified from:body -> ermite.layout('unified'); | |
| 454 | + | k-collapse from:body -> ermite.collapse(); | |
| 455 | + | k-permalink from:body -> ermite.permalink()"> | |
| 502 | 456 | </div> | |
| 503 | 457 | </body> | |
| 504 | 458 | </html> | |
M cmd/ermite/testdata/web/commit-root.html +35 -81
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/diffs/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">detached</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -100,11 +100,11 @@ | |||
| 100 | 100 | </span> | |
| 101 | 101 | <span class="grow"> | |
| 102 | 102 | </span> | |
| 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 aria-hidden="true">u</kbd> | |
| 103 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 104 | 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 aria-hidden="true">d</kbd> | |
| 105 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 106 | 106 | </a> | |
| 107 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn"> | |
| 107 | + | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 108 | 108 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| 109 | 109 | <kbd aria-hidden="true">x</kbd> | |
| 110 | 110 | </button> | |
| @@ -454,7 +454,7 @@ | |||
| 454 | 454 | </h2> | |
| 455 | 455 | <ol class="files" aria-label="Changed files"> | |
| 456 | 456 | <li> | |
| 457 | - | <a href="#f1" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f1' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 457 | + | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 458 | 458 | <span class="st" title="A">A</span> | |
| 459 | 459 | <span class="p" title="a.txt">a.txt</span> | |
| 460 | 460 | <span class="n"> | |
| @@ -469,7 +469,7 @@ | |||
| 469 | 469 | </a> | |
| 470 | 470 | </li> | |
| 471 | 471 | <li> | |
| 472 | - | <a href="#f2" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f2' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 472 | + | <a href="#f2" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 473 | 473 | <span class="st" title="A">A</span> | |
| 474 | 474 | <span class="p" title="gone.txt">gone.txt</span> | |
| 475 | 475 | <span class="n"> | |
| @@ -483,7 +483,7 @@ | |||
| 483 | 483 | </a> | |
| 484 | 484 | </li> | |
| 485 | 485 | <li> | |
| 486 | - | <a href="#f3" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f3' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 486 | + | <a href="#f3" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 487 | 487 | <span class="st" title="A">A</span> | |
| 488 | 488 | <span class="p" title="logo.bin">logo.bin</span> | |
| 489 | 489 | <span class="n"> | |
| @@ -498,7 +498,7 @@ | |||
| 498 | 498 | </a> | |
| 499 | 499 | </li> | |
| 500 | 500 | <li> | |
| 501 | - | <a href="#f4" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f4' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 501 | + | <a href="#f4" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 502 | 502 | <span class="st" title="A">A</span> | |
| 503 | 503 | <span class="p" title="old.txt">old.txt</span> | |
| 504 | 504 | <span class="n"> | |
| @@ -512,7 +512,7 @@ | |||
| 512 | 512 | </a> | |
| 513 | 513 | </li> | |
| 514 | 514 | <li> | |
| 515 | - | <a href="#f5" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f5' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 515 | + | <a href="#f5" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 516 | 516 | <span class="st" title="A">A</span> | |
| 517 | 517 | <span class="p" title="run.sh">run.sh</span> | |
| 518 | 518 | <span class="n"> | |
| @@ -642,7 +642,7 @@ | |||
| 642 | 642 | <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()"> | |
| 643 | 643 | <div class="panel-head"> | |
| 644 | 644 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 645 | - | <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))"> | |
| 645 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 646 | 646 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 647 | 647 | <kbd>esc</kbd> | |
| 648 | 648 | </button> | |
| @@ -677,13 +677,7 @@ | |||
| 677 | 677 | <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()"> | |
| 678 | 678 | <div class="panel-head"> | |
| 679 | 679 | <label for="cmd-q" class="sr">go to</label> | |
| 680 | - | <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(); | |
| 681 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 682 | - | 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(); | |
| 683 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 684 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 685 | - | 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' })) } | |
| 686 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 680 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 687 | 681 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 688 | 682 | <kbd>esc</kbd> | |
| 689 | 683 | </button> | |
| @@ -754,67 +748,27 @@ | |||
| 754 | 748 | <kbd>esc</kbd> close</span> | |
| 755 | 749 | </p> | |
| 756 | 750 | </dialog> | |
| 757 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 758 | - | if (q('dialog[open]').count) return; | |
| 759 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 760 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 761 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 762 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 763 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 764 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 765 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 766 | - | })(1); | |
| 767 | - | k-k from:body -> (async (d) => { | |
| 768 | - | if (q('dialog[open]').count) return; | |
| 769 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 770 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 771 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 772 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 773 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 774 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 775 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 776 | - | })(-1); | |
| 777 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 778 | - | 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)); | |
| 779 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 780 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 781 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 782 | - | 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()); | |
| 783 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 784 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 785 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 786 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 787 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 788 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 789 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 790 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 791 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 792 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 793 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 794 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 795 | - | k-next from:body -> { | |
| 796 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 797 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 798 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 799 | - | }; | |
| 800 | - | k-split from:body -> { | |
| 801 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 802 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 803 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 804 | - | }; | |
| 805 | - | k-unified from:body -> { | |
| 806 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 807 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 808 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 809 | - | }; | |
| 810 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 811 | - | k-permalink from:body -> { | |
| 812 | - | let b = document.body.dataset; | |
| 813 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 814 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 815 | - | navigator.clipboard?.writeText(location.href); | |
| 816 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 817 | - | }"> | |
| 751 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 752 | + | k-k from:body -> ermite.move(-1); | |
| 753 | + | k-top from:body -> ermite.edge(false); | |
| 754 | + | k-bottom from:body -> ermite.edge(true); | |
| 755 | + | k-open from:body -> ermite.open(); | |
| 756 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 757 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 758 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 759 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 760 | + | k-filter from:body -> ermite.focusFilter(); | |
| 761 | + | k-gh from:body -> ermite.go('.brand'); | |
| 762 | + | k-s from:body -> ermite.tab('s'); | |
| 763 | + | k-t from:body -> ermite.tab('t'); | |
| 764 | + | k-c from:body -> ermite.tab('c'); | |
| 765 | + | k-r from:body -> ermite.tab('r'); | |
| 766 | + | k-m from:body -> ermite.tab('m'); | |
| 767 | + | k-next from:body -> ermite.nextNote(); | |
| 768 | + | k-split from:body -> ermite.layout('split'); | |
| 769 | + | k-unified from:body -> ermite.layout('unified'); | |
| 770 | + | k-collapse from:body -> ermite.collapse(); | |
| 771 | + | k-permalink from:body -> ermite.permalink()"> | |
| 818 | 772 | </div> | |
| 819 | 773 | </body> | |
| 820 | 774 | </html> | |
M cmd/ermite/testdata/web/commit-trojan.html +31 -77
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/diffs/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">detached</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -103,11 +103,11 @@ | |||
| 103 | 103 | </span> | |
| 104 | 104 | <span class="grow"> | |
| 105 | 105 | </span> | |
| 106 | - | <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 aria-hidden="true">u</kbd> | |
| 106 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 107 | 107 | </a> | |
| 108 | - | <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 aria-hidden="true">d</kbd> | |
| 108 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 109 | 109 | </a> | |
| 110 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn"> | |
| 110 | + | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 111 | 111 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| 112 | 112 | <kbd aria-hidden="true">x</kbd> | |
| 113 | 113 | </button> | |
| @@ -304,7 +304,7 @@ | |||
| 304 | 304 | </h2> | |
| 305 | 305 | <ol class="files" aria-label="Changed files"> | |
| 306 | 306 | <li> | |
| 307 | - | <a href="#f1" :aria-current="(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === 'f1' ? 'true' : false })()" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 307 | + | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 308 | 308 | <span class="st" title="A">A</span> | |
| 309 | 309 | <span class="p" title="evil.go">evil.go</span> | |
| 310 | 310 | <span class="n"> | |
| @@ -435,7 +435,7 @@ | |||
| 435 | 435 | <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 | 436 | <div class="panel-head"> | |
| 437 | 437 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 438 | - | <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))"> | |
| 438 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 439 | 439 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 440 | 440 | <kbd>esc</kbd> | |
| 441 | 441 | </button> | |
| @@ -470,13 +470,7 @@ | |||
| 470 | 470 | <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()"> | |
| 471 | 471 | <div class="panel-head"> | |
| 472 | 472 | <label for="cmd-q" class="sr">go to</label> | |
| 473 | - | <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(); | |
| 474 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 475 | - | 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(); | |
| 476 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 477 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 478 | - | 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' })) } | |
| 479 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 473 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 480 | 474 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 481 | 475 | <kbd>esc</kbd> | |
| 482 | 476 | </button> | |
| @@ -547,67 +541,27 @@ | |||
| 547 | 541 | <kbd>esc</kbd> close</span> | |
| 548 | 542 | </p> | |
| 549 | 543 | </dialog> | |
| 550 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 551 | - | if (q('dialog[open]').count) return; | |
| 552 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 553 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 554 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 555 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 556 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 557 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 558 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 559 | - | })(1); | |
| 560 | - | k-k from:body -> (async (d) => { | |
| 561 | - | if (q('dialog[open]').count) return; | |
| 562 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 563 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 564 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 565 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 566 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 567 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 568 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 569 | - | })(-1); | |
| 570 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 571 | - | 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)); | |
| 572 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 573 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 574 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 575 | - | 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()); | |
| 576 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 577 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 578 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 579 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 580 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 581 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 582 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 583 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 584 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 585 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 586 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 587 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 588 | - | k-next from:body -> { | |
| 589 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 590 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 591 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 592 | - | }; | |
| 593 | - | k-split from:body -> { | |
| 594 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 595 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 596 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 597 | - | }; | |
| 598 | - | k-unified from:body -> { | |
| 599 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 600 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 601 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 602 | - | }; | |
| 603 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 604 | - | k-permalink from:body -> { | |
| 605 | - | let b = document.body.dataset; | |
| 606 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 607 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 608 | - | navigator.clipboard?.writeText(location.href); | |
| 609 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 610 | - | }"> | |
| 544 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 545 | + | k-k from:body -> ermite.move(-1); | |
| 546 | + | k-top from:body -> ermite.edge(false); | |
| 547 | + | k-bottom from:body -> ermite.edge(true); | |
| 548 | + | k-open from:body -> ermite.open(); | |
| 549 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 550 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 551 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 552 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 553 | + | k-filter from:body -> ermite.focusFilter(); | |
| 554 | + | k-gh from:body -> ermite.go('.brand'); | |
| 555 | + | k-s from:body -> ermite.tab('s'); | |
| 556 | + | k-t from:body -> ermite.tab('t'); | |
| 557 | + | k-c from:body -> ermite.tab('c'); | |
| 558 | + | k-r from:body -> ermite.tab('r'); | |
| 559 | + | k-m from:body -> ermite.tab('m'); | |
| 560 | + | k-next from:body -> ermite.nextNote(); | |
| 561 | + | k-split from:body -> ermite.layout('split'); | |
| 562 | + | k-unified from:body -> ermite.layout('unified'); | |
| 563 | + | k-collapse from:body -> ermite.collapse(); | |
| 564 | + | k-permalink from:body -> ermite.permalink()"> | |
| 611 | 565 | </div> | |
| 612 | 566 | </body> | |
| 613 | 567 | </html> | |
M cmd/ermite/testdata/web/commits-page2.html +28 -76
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/long/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">main</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -85,9 +85,7 @@ | |||
| 85 | 85 | <main id="main" tabindex="-1"> | |
| 86 | 86 | <div class="filter"> | |
| 87 | 87 | <label for="commit-filter" class="sr">filter</label> | |
| 88 | - | <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)); | |
| 89 | - | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | |
| 90 | - | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | |
| 88 | + | <input id="commit-filter" type="search" data-filter=".days tr" data-groups=".days section" placeholder="Filter by subject, sha or author…" autocomplete="off" hx-on:input="ermite.filter(this)"> | |
| 91 | 89 | </div> | |
| 92 | 90 | <div class="days"> | |
| 93 | 91 | <section aria-label="2026-03-01"> | |
| @@ -258,7 +256,7 @@ | |||
| 258 | 256 | <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()"> | |
| 259 | 257 | <div class="panel-head"> | |
| 260 | 258 | <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))"> | |
| 259 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 262 | 260 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 263 | 261 | <kbd>esc</kbd> | |
| 264 | 262 | </button> | |
| @@ -285,13 +283,7 @@ | |||
| 285 | 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()"> | |
| 286 | 284 | <div class="panel-head"> | |
| 287 | 285 | <label for="cmd-q" class="sr">go to</label> | |
| 288 | - | <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(); | |
| 289 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 290 | - | 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(); | |
| 291 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 292 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 293 | - | 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' })) } | |
| 294 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 295 | 287 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 296 | 288 | <kbd>esc</kbd> | |
| 297 | 289 | </button> | |
| @@ -356,67 +348,27 @@ | |||
| 356 | 348 | <kbd>esc</kbd> close</span> | |
| 357 | 349 | </p> | |
| 358 | 350 | </dialog> | |
| 359 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 360 | - | if (q('dialog[open]').count) return; | |
| 361 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 362 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 363 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 364 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 365 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 366 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 367 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 368 | - | })(1); | |
| 369 | - | k-k from:body -> (async (d) => { | |
| 370 | - | if (q('dialog[open]').count) return; | |
| 371 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 372 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 373 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 374 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 375 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 376 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 377 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 378 | - | })(-1); | |
| 379 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 380 | - | 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)); | |
| 381 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 382 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 383 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 384 | - | 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()); | |
| 385 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 386 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 387 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 388 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 389 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 390 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 391 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 392 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 393 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 394 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 395 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 396 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 397 | - | k-next from:body -> { | |
| 398 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 399 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 400 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 401 | - | }; | |
| 402 | - | k-split from:body -> { | |
| 403 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 404 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 405 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 406 | - | }; | |
| 407 | - | k-unified from:body -> { | |
| 408 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 409 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 410 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 411 | - | }; | |
| 412 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 413 | - | k-permalink from:body -> { | |
| 414 | - | let b = document.body.dataset; | |
| 415 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 416 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 417 | - | navigator.clipboard?.writeText(location.href); | |
| 418 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 419 | - | }"> | |
| 351 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 352 | + | k-k from:body -> ermite.move(-1); | |
| 353 | + | k-top from:body -> ermite.edge(false); | |
| 354 | + | k-bottom from:body -> ermite.edge(true); | |
| 355 | + | k-open from:body -> ermite.open(); | |
| 356 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 357 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 358 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 359 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 360 | + | k-filter from:body -> ermite.focusFilter(); | |
| 361 | + | k-gh from:body -> ermite.go('.brand'); | |
| 362 | + | k-s from:body -> ermite.tab('s'); | |
| 363 | + | k-t from:body -> ermite.tab('t'); | |
| 364 | + | k-c from:body -> ermite.tab('c'); | |
| 365 | + | k-r from:body -> ermite.tab('r'); | |
| 366 | + | k-m from:body -> ermite.tab('m'); | |
| 367 | + | k-next from:body -> ermite.nextNote(); | |
| 368 | + | k-split from:body -> ermite.layout('split'); | |
| 369 | + | k-unified from:body -> ermite.layout('unified'); | |
| 370 | + | k-collapse from:body -> ermite.collapse(); | |
| 371 | + | k-permalink from:body -> ermite.permalink()"> | |
| 420 | 372 | </div> | |
| 421 | 373 | </body> | |
| 422 | 374 | </html> | |
M cmd/ermite/testdata/web/commits-path.html +28 -76
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/long/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">main</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -88,9 +88,7 @@ | |||
| 88 | 88 | <main id="main" tabindex="-1"> | |
| 89 | 89 | <div class="filter"> | |
| 90 | 90 | <label for="commit-filter" class="sr">filter</label> | |
| 91 | - | <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)); | |
| 92 | - | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | |
| 93 | - | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | |
| 91 | + | <input id="commit-filter" type="search" data-filter=".days tr" data-groups=".days section" placeholder="Filter by subject, sha or author…" autocomplete="off" hx-on:input="ermite.filter(this)"> | |
| 94 | 92 | </div> | |
| 95 | 93 | <p class="lede dim">history of <code>a.txt</code> at main</p> | |
| 96 | 94 | <div class="days"> | |
| @@ -273,7 +271,7 @@ | |||
| 273 | 271 | <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()"> | |
| 274 | 272 | <div class="panel-head"> | |
| 275 | 273 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 276 | - | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))"> | |
| 274 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 277 | 275 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 278 | 276 | <kbd>esc</kbd> | |
| 279 | 277 | </button> | |
| @@ -300,13 +298,7 @@ | |||
| 300 | 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()"> | |
| 301 | 299 | <div class="panel-head"> | |
| 302 | 300 | <label for="cmd-q" class="sr">go to</label> | |
| 303 | - | <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(); | |
| 304 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 305 | - | 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(); | |
| 306 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 307 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 308 | - | 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' })) } | |
| 309 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 310 | 302 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 311 | 303 | <kbd>esc</kbd> | |
| 312 | 304 | </button> | |
| @@ -371,67 +363,27 @@ | |||
| 371 | 363 | <kbd>esc</kbd> close</span> | |
| 372 | 364 | </p> | |
| 373 | 365 | </dialog> | |
| 374 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 375 | - | if (q('dialog[open]').count) return; | |
| 376 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 377 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 378 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 379 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 380 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 381 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 382 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 383 | - | })(1); | |
| 384 | - | k-k from:body -> (async (d) => { | |
| 385 | - | if (q('dialog[open]').count) return; | |
| 386 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 387 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 388 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 389 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 390 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 391 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 392 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 393 | - | })(-1); | |
| 394 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 395 | - | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | |
| 396 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 397 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 398 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 399 | - | k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus()); | |
| 400 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 401 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 402 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 403 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 404 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 405 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 406 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 407 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 408 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 409 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 410 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 411 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 412 | - | k-next from:body -> { | |
| 413 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 414 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 415 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 416 | - | }; | |
| 417 | - | k-split from:body -> { | |
| 418 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 419 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 420 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 421 | - | }; | |
| 422 | - | k-unified from:body -> { | |
| 423 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 424 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 425 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 426 | - | }; | |
| 427 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 428 | - | k-permalink from:body -> { | |
| 429 | - | let b = document.body.dataset; | |
| 430 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 431 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 432 | - | navigator.clipboard?.writeText(location.href); | |
| 433 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 434 | - | }"> | |
| 366 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 367 | + | k-k from:body -> ermite.move(-1); | |
| 368 | + | k-top from:body -> ermite.edge(false); | |
| 369 | + | k-bottom from:body -> ermite.edge(true); | |
| 370 | + | k-open from:body -> ermite.open(); | |
| 371 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 372 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 373 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 374 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 375 | + | k-filter from:body -> ermite.focusFilter(); | |
| 376 | + | k-gh from:body -> ermite.go('.brand'); | |
| 377 | + | k-s from:body -> ermite.tab('s'); | |
| 378 | + | k-t from:body -> ermite.tab('t'); | |
| 379 | + | k-c from:body -> ermite.tab('c'); | |
| 380 | + | k-r from:body -> ermite.tab('r'); | |
| 381 | + | k-m from:body -> ermite.tab('m'); | |
| 382 | + | k-next from:body -> ermite.nextNote(); | |
| 383 | + | k-split from:body -> ermite.layout('split'); | |
| 384 | + | k-unified from:body -> ermite.layout('unified'); | |
| 385 | + | k-collapse from:body -> ermite.collapse(); | |
| 386 | + | k-permalink from:body -> ermite.permalink()"> | |
| 435 | 387 | </div> | |
| 436 | 388 | </body> | |
| 437 | 389 | </html> | |
M cmd/ermite/testdata/web/commits-root.html +31 -77
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/long/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">main</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -85,11 +85,9 @@ | |||
| 85 | 85 | <main id="main" tabindex="-1"> | |
| 86 | 86 | <div class="filter"> | |
| 87 | 87 | <label for="commit-filter" class="sr">filter</label> | |
| 88 | - | <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)); | |
| 89 | - | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | |
| 90 | - | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | |
| 88 | + | <input id="commit-filter" type="search" data-filter=".days tr" data-groups=".days section" placeholder="Filter by subject, sha or author…" autocomplete="off" hx-on:input="ermite.filter(this)"> | |
| 91 | 89 | </div> | |
| 92 | - | <div id="commits-head"> | |
| 90 | + | <div id="commits-head" class="live-slot" role="status" data-slot> | |
| 93 | 91 | </div> | |
| 94 | 92 | <div class="days"> | |
| 95 | 93 | <section aria-label="2026-03-26"> | |
| @@ -957,6 +955,8 @@ | |||
| 957 | 955 | </span> | |
| 958 | 956 | <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'; | |
| 959 | 957 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 958 | + | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 959 | + | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 960 | 960 | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 961 | 961 | gap -> ermiteLive.reload()"> | |
| 962 | 962 | </span> | |
| @@ -1060,7 +1060,7 @@ | |||
| 1060 | 1060 | <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()"> | |
| 1061 | 1061 | <div class="panel-head"> | |
| 1062 | 1062 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 1063 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 1064 | 1064 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 1065 | 1065 | <kbd>esc</kbd> | |
| 1066 | 1066 | </button> | |
| @@ -1087,13 +1087,7 @@ | |||
| 1087 | 1087 | <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()"> | |
| 1088 | 1088 | <div class="panel-head"> | |
| 1089 | 1089 | <label for="cmd-q" class="sr">go to</label> | |
| 1090 | - | <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(); | |
| 1091 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 1092 | - | 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(); | |
| 1093 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 1094 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 1095 | - | 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' })) } | |
| 1096 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 1090 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 1097 | 1091 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 1098 | 1092 | <kbd>esc</kbd> | |
| 1099 | 1093 | </button> | |
| @@ -1158,67 +1152,27 @@ | |||
| 1158 | 1152 | <kbd>esc</kbd> close</span> | |
| 1159 | 1153 | </p> | |
| 1160 | 1154 | </dialog> | |
| 1161 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 1162 | - | if (q('dialog[open]').count) return; | |
| 1163 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 1164 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 1165 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 1166 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 1167 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 1168 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 1169 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 1170 | - | })(1); | |
| 1171 | - | k-k from:body -> (async (d) => { | |
| 1172 | - | if (q('dialog[open]').count) return; | |
| 1173 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 1174 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 1175 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 1176 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 1177 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 1178 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 1179 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 1180 | - | })(-1); | |
| 1181 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 1182 | - | 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)); | |
| 1183 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 1184 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 1185 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 1186 | - | 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()); | |
| 1187 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 1188 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 1189 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 1190 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 1191 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 1192 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 1193 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 1194 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 1195 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 1196 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 1197 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 1198 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 1199 | - | k-next from:body -> { | |
| 1200 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 1201 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 1202 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 1203 | - | }; | |
| 1204 | - | k-split from:body -> { | |
| 1205 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 1206 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 1207 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 1208 | - | }; | |
| 1209 | - | k-unified from:body -> { | |
| 1210 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 1211 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 1212 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 1213 | - | }; | |
| 1214 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 1215 | - | k-permalink from:body -> { | |
| 1216 | - | let b = document.body.dataset; | |
| 1217 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 1218 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 1219 | - | navigator.clipboard?.writeText(location.href); | |
| 1220 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 1221 | - | }"> | |
| 1155 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 1156 | + | k-k from:body -> ermite.move(-1); | |
| 1157 | + | k-top from:body -> ermite.edge(false); | |
| 1158 | + | k-bottom from:body -> ermite.edge(true); | |
| 1159 | + | k-open from:body -> ermite.open(); | |
| 1160 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 1161 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 1162 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 1163 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 1164 | + | k-filter from:body -> ermite.focusFilter(); | |
| 1165 | + | k-gh from:body -> ermite.go('.brand'); | |
| 1166 | + | k-s from:body -> ermite.tab('s'); | |
| 1167 | + | k-t from:body -> ermite.tab('t'); | |
| 1168 | + | k-c from:body -> ermite.tab('c'); | |
| 1169 | + | k-r from:body -> ermite.tab('r'); | |
| 1170 | + | k-m from:body -> ermite.tab('m'); | |
| 1171 | + | k-next from:body -> ermite.nextNote(); | |
| 1172 | + | k-split from:body -> ermite.layout('split'); | |
| 1173 | + | k-unified from:body -> ermite.layout('unified'); | |
| 1174 | + | k-collapse from:body -> ermite.collapse(); | |
| 1175 | + | k-permalink from:body -> ermite.permalink()"> | |
| 1222 | 1176 | </div> | |
| 1223 | 1177 | </body> | |
| 1224 | 1178 | </html> | |
M cmd/ermite/testdata/web/commits-small.html +31 -77
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/diffs/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">main</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -85,11 +85,9 @@ | |||
| 85 | 85 | <main id="main" tabindex="-1"> | |
| 86 | 86 | <div class="filter"> | |
| 87 | 87 | <label for="commit-filter" class="sr">filter</label> | |
| 88 | - | <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)); | |
| 89 | - | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | |
| 90 | - | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')"> | |
| 88 | + | <input id="commit-filter" type="search" data-filter=".days tr" data-groups=".days section" placeholder="Filter by subject, sha or author…" autocomplete="off" hx-on:input="ermite.filter(this)"> | |
| 91 | 89 | </div> | |
| 92 | - | <div id="commits-head"> | |
| 90 | + | <div id="commits-head" class="live-slot" role="status" data-slot> | |
| 93 | 91 | </div> | |
| 94 | 92 | <div class="days"> | |
| 95 | 93 | <section aria-label="2026-02-08"> | |
| @@ -302,6 +300,8 @@ | |||
| 302 | 300 | </span> | |
| 303 | 301 | <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'; | |
| 304 | 302 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 303 | + | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 304 | + | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 305 | 305 | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 306 | 306 | gap -> ermiteLive.reload()"> | |
| 307 | 307 | </span> | |
| @@ -405,7 +405,7 @@ | |||
| 405 | 405 | <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()"> | |
| 406 | 406 | <div class="panel-head"> | |
| 407 | 407 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 408 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 409 | 409 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 410 | 410 | <kbd>esc</kbd> | |
| 411 | 411 | </button> | |
| @@ -440,13 +440,7 @@ | |||
| 440 | 440 | <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()"> | |
| 441 | 441 | <div class="panel-head"> | |
| 442 | 442 | <label for="cmd-q" class="sr">go to</label> | |
| 443 | - | <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(); | |
| 444 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 445 | - | 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(); | |
| 446 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 447 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 448 | - | 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' })) } | |
| 449 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 443 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 450 | 444 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 451 | 445 | <kbd>esc</kbd> | |
| 452 | 446 | </button> | |
| @@ -523,67 +517,27 @@ | |||
| 523 | 517 | <kbd>esc</kbd> close</span> | |
| 524 | 518 | </p> | |
| 525 | 519 | </dialog> | |
| 526 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 527 | - | if (q('dialog[open]').count) return; | |
| 528 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 529 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 530 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 531 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 532 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 533 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 534 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 535 | - | })(1); | |
| 536 | - | k-k from:body -> (async (d) => { | |
| 537 | - | if (q('dialog[open]').count) return; | |
| 538 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 539 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 540 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 541 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 542 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 543 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 544 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 545 | - | })(-1); | |
| 546 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 547 | - | 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)); | |
| 548 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 549 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 550 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 551 | - | 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()); | |
| 552 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 553 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 554 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 555 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 556 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 557 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 558 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 559 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 560 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 561 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 562 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 563 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 564 | - | k-next from:body -> { | |
| 565 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 566 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 567 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 568 | - | }; | |
| 569 | - | k-split from:body -> { | |
| 570 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 571 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 572 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 573 | - | }; | |
| 574 | - | k-unified from:body -> { | |
| 575 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 576 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 577 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 578 | - | }; | |
| 579 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 580 | - | k-permalink from:body -> { | |
| 581 | - | let b = document.body.dataset; | |
| 582 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 583 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 584 | - | navigator.clipboard?.writeText(location.href); | |
| 585 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 586 | - | }"> | |
| 520 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 521 | + | k-k from:body -> ermite.move(-1); | |
| 522 | + | k-top from:body -> ermite.edge(false); | |
| 523 | + | k-bottom from:body -> ermite.edge(true); | |
| 524 | + | k-open from:body -> ermite.open(); | |
| 525 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 526 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 527 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 528 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 529 | + | k-filter from:body -> ermite.focusFilter(); | |
| 530 | + | k-gh from:body -> ermite.go('.brand'); | |
| 531 | + | k-s from:body -> ermite.tab('s'); | |
| 532 | + | k-t from:body -> ermite.tab('t'); | |
| 533 | + | k-c from:body -> ermite.tab('c'); | |
| 534 | + | k-r from:body -> ermite.tab('r'); | |
| 535 | + | k-m from:body -> ermite.tab('m'); | |
| 536 | + | k-next from:body -> ermite.nextNote(); | |
| 537 | + | k-split from:body -> ermite.layout('split'); | |
| 538 | + | k-unified from:body -> ermite.layout('unified'); | |
| 539 | + | k-collapse from:body -> ermite.collapse(); | |
| 540 | + | k-permalink from:body -> ermite.permalink()"> | |
| 587 | 541 | </div> | |
| 588 | 542 | </body> | |
| 589 | 543 | </html> | |
M cmd/ermite/testdata/web/empty.html +26 -72
| @@ -35,13 +35,13 @@ | |||
| 35 | 35 | </ol> | |
| 36 | 36 | </nav> | |
| 37 | 37 | <div class="hdr-acts"> | |
| 38 | - | <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 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 39 | 39 | <span class="ico i-search" aria-hidden="true"> | |
| 40 | 40 | </span> | |
| 41 | 41 | <span class="search-label">Go to…</span> | |
| 42 | 42 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 43 | 43 | </button> | |
| 44 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 44 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 45 | 45 | </div> | |
| 46 | 46 | </div> | |
| 47 | 47 | </header> | |
| @@ -55,12 +55,12 @@ | |||
| 55 | 55 | <div class="clone-line"> | |
| 56 | 56 | <span class="mute" aria-hidden="true">$ </span> | |
| 57 | 57 | <code>git remote add origin http://example.test/quiet.git</code> | |
| 58 | - | <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> | |
| 58 | + | <button type="button" class="btn copy" data-js data-label="Copy" aria-label="copy: git remote add origin http://example.test/quiet.git" hx-on:click="ermite.copy(this)">Copy</button> | |
| 59 | 59 | </div> | |
| 60 | 60 | <div class="clone-line"> | |
| 61 | 61 | <span class="mute" aria-hidden="true">$ </span> | |
| 62 | 62 | <code>git push -u origin main</code> | |
| 63 | - | <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> | |
| 63 | + | <button type="button" class="btn copy" data-js data-label="Copy" aria-label="copy: git push -u origin main" hx-on:click="ermite.copy(this)">Copy</button> | |
| 64 | 64 | </div> | |
| 65 | 65 | </div> | |
| 66 | 66 | <p class="state-back"> | |
| @@ -182,13 +182,7 @@ | |||
| 182 | 182 | <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()"> | |
| 183 | 183 | <div class="panel-head"> | |
| 184 | 184 | <label for="cmd-q" class="sr">go to</label> | |
| 185 | - | <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(); | |
| 186 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 187 | - | 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(); | |
| 188 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 189 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 190 | - | 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' })) } | |
| 191 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 185 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 192 | 186 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 193 | 187 | <kbd>esc</kbd> | |
| 194 | 188 | </button> | |
| @@ -217,67 +211,27 @@ | |||
| 217 | 211 | <kbd>esc</kbd> close</span> | |
| 218 | 212 | </p> | |
| 219 | 213 | </dialog> | |
| 220 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 221 | - | if (q('dialog[open]').count) return; | |
| 222 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 223 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 224 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 225 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 226 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 227 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 228 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 229 | - | })(1); | |
| 230 | - | k-k from:body -> (async (d) => { | |
| 231 | - | if (q('dialog[open]').count) return; | |
| 232 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 233 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 234 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 235 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 236 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 237 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 238 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 239 | - | })(-1); | |
| 240 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 241 | - | 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)); | |
| 242 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 243 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 244 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 245 | - | 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()); | |
| 246 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 247 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 248 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 249 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 250 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 251 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 252 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 253 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 254 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 255 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 256 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 257 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 258 | - | k-next from:body -> { | |
| 259 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 260 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 261 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 262 | - | }; | |
| 263 | - | k-split from:body -> { | |
| 264 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 265 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 266 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 267 | - | }; | |
| 268 | - | k-unified from:body -> { | |
| 269 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 270 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 271 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 272 | - | }; | |
| 273 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 274 | - | k-permalink from:body -> { | |
| 275 | - | let b = document.body.dataset; | |
| 276 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 277 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 278 | - | navigator.clipboard?.writeText(location.href); | |
| 279 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 280 | - | }"> | |
| 214 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 215 | + | k-k from:body -> ermite.move(-1); | |
| 216 | + | k-top from:body -> ermite.edge(false); | |
| 217 | + | k-bottom from:body -> ermite.edge(true); | |
| 218 | + | k-open from:body -> ermite.open(); | |
| 219 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 220 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 221 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 222 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 223 | + | k-filter from:body -> ermite.focusFilter(); | |
| 224 | + | k-gh from:body -> ermite.go('.brand'); | |
| 225 | + | k-s from:body -> ermite.tab('s'); | |
| 226 | + | k-t from:body -> ermite.tab('t'); | |
| 227 | + | k-c from:body -> ermite.tab('c'); | |
| 228 | + | k-r from:body -> ermite.tab('r'); | |
| 229 | + | k-m from:body -> ermite.tab('m'); | |
| 230 | + | k-next from:body -> ermite.nextNote(); | |
| 231 | + | k-split from:body -> ermite.layout('split'); | |
| 232 | + | k-unified from:body -> ermite.layout('unified'); | |
| 233 | + | k-collapse from:body -> ermite.collapse(); | |
| 234 | + | k-permalink from:body -> ermite.permalink()"> | |
| 281 | 235 | </div> | |
| 282 | 236 | </body> | |
| 283 | 237 | </html> | |
M cmd/ermite/testdata/web/home.html +27 -71
| @@ -28,13 +28,13 @@ | |||
| 28 | 28 | <span class="brand-name">example.test</span> | |
| 29 | 29 | </a> | |
| 30 | 30 | <div class="hdr-acts"> | |
| 31 | - | <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 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 32 | 32 | <span class="ico i-search" aria-hidden="true"> | |
| 33 | 33 | </span> | |
| 34 | 34 | <span class="search-label">Go to…</span> | |
| 35 | 35 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 36 | 36 | </button> | |
| 37 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 37 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 38 | 38 | </div> | |
| 39 | 39 | </div> | |
| 40 | 40 | </header> | |
| @@ -42,7 +42,7 @@ | |||
| 42 | 42 | <main id="main" tabindex="-1"> | |
| 43 | 43 | <div class="filter"> | |
| 44 | 44 | <label for="home-filter" class="sr">filter</label> | |
| 45 | - | <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])')"> | |
| 45 | + | <input id="home-filter" type="search" data-filter="#repo-rows > tr" placeholder="Filter repositories…" autocomplete="off" hx-on:input="ermite.filter(this)"> | |
| 46 | 46 | </div> | |
| 47 | 47 | <table class="list home" aria-label="Repositories"> | |
| 48 | 48 | <thead> | |
| @@ -109,6 +109,8 @@ | |||
| 109 | 109 | </span> | |
| 110 | 110 | <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'; | |
| 111 | 111 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 112 | + | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 113 | + | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 112 | 114 | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 113 | 115 | gap -> ermiteLive.reload()"> | |
| 114 | 116 | </span> | |
| @@ -212,13 +214,7 @@ | |||
| 212 | 214 | <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()"> | |
| 213 | 215 | <div class="panel-head"> | |
| 214 | 216 | <label for="cmd-q" class="sr">go to</label> | |
| 215 | - | <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(); | |
| 216 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 217 | - | 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(); | |
| 218 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 219 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 220 | - | 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' })) } | |
| 221 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 217 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 222 | 218 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 223 | 219 | <kbd>esc</kbd> | |
| 224 | 220 | </button> | |
| @@ -253,67 +249,27 @@ | |||
| 253 | 249 | <kbd>esc</kbd> close</span> | |
| 254 | 250 | </p> | |
| 255 | 251 | </dialog> | |
| 256 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 257 | - | if (q('dialog[open]').count) return; | |
| 258 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 259 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 260 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 261 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 262 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 263 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 264 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 265 | - | })(1); | |
| 266 | - | k-k from:body -> (async (d) => { | |
| 267 | - | if (q('dialog[open]').count) return; | |
| 268 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 269 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 270 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 271 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 272 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 273 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 274 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 275 | - | })(-1); | |
| 276 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 277 | - | 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)); | |
| 278 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 279 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 280 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 281 | - | 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()); | |
| 282 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 283 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 284 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 285 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 286 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 287 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 288 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 289 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 290 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 291 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 292 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 293 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 294 | - | k-next from:body -> { | |
| 295 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 296 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 297 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 298 | - | }; | |
| 299 | - | k-split from:body -> { | |
| 300 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 301 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 302 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 303 | - | }; | |
| 304 | - | k-unified from:body -> { | |
| 305 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 306 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 307 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 308 | - | }; | |
| 309 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 310 | - | k-permalink from:body -> { | |
| 311 | - | let b = document.body.dataset; | |
| 312 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 313 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 314 | - | navigator.clipboard?.writeText(location.href); | |
| 315 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 316 | - | }"> | |
| 252 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 253 | + | k-k from:body -> ermite.move(-1); | |
| 254 | + | k-top from:body -> ermite.edge(false); | |
| 255 | + | k-bottom from:body -> ermite.edge(true); | |
| 256 | + | k-open from:body -> ermite.open(); | |
| 257 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 258 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 259 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 260 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 261 | + | k-filter from:body -> ermite.focusFilter(); | |
| 262 | + | k-gh from:body -> ermite.go('.brand'); | |
| 263 | + | k-s from:body -> ermite.tab('s'); | |
| 264 | + | k-t from:body -> ermite.tab('t'); | |
| 265 | + | k-c from:body -> ermite.tab('c'); | |
| 266 | + | k-r from:body -> ermite.tab('r'); | |
| 267 | + | k-m from:body -> ermite.tab('m'); | |
| 268 | + | k-next from:body -> ermite.nextNote(); | |
| 269 | + | k-split from:body -> ermite.layout('split'); | |
| 270 | + | k-unified from:body -> ermite.layout('unified'); | |
| 271 | + | k-collapse from:body -> ermite.collapse(); | |
| 272 | + | k-permalink from:body -> ermite.permalink()"> | |
| 317 | 273 | </div> | |
| 318 | 274 | </body> | |
| 319 | 275 | </html> | |
M cmd/ermite/testdata/web/not-found-page.html +24 -70
| @@ -35,13 +35,13 @@ | |||
| 35 | 35 | </ol> | |
| 36 | 36 | </nav> | |
| 37 | 37 | <div class="hdr-acts"> | |
| 38 | - | <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 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 39 | 39 | <span class="ico i-search" aria-hidden="true"> | |
| 40 | 40 | </span> | |
| 41 | 41 | <span class="search-label">Go to…</span> | |
| 42 | 42 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 43 | 43 | </button> | |
| 44 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 44 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 45 | 45 | </div> | |
| 46 | 46 | </div> | |
| 47 | 47 | </header> | |
| @@ -171,13 +171,7 @@ | |||
| 171 | 171 | <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()"> | |
| 172 | 172 | <div class="panel-head"> | |
| 173 | 173 | <label for="cmd-q" class="sr">go to</label> | |
| 174 | - | <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(); | |
| 175 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 176 | - | 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(); | |
| 177 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 178 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 179 | - | 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' })) } | |
| 180 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 174 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 181 | 175 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 182 | 176 | <kbd>esc</kbd> | |
| 183 | 177 | </button> | |
| @@ -200,67 +194,27 @@ | |||
| 200 | 194 | <kbd>esc</kbd> close</span> | |
| 201 | 195 | </p> | |
| 202 | 196 | </dialog> | |
| 203 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 204 | - | if (q('dialog[open]').count) return; | |
| 205 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 206 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 207 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 208 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 209 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 210 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 211 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 212 | - | })(1); | |
| 213 | - | k-k from:body -> (async (d) => { | |
| 214 | - | if (q('dialog[open]').count) return; | |
| 215 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 216 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 217 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 218 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 219 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 220 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 221 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 222 | - | })(-1); | |
| 223 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 224 | - | 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)); | |
| 225 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 226 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 227 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 228 | - | 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()); | |
| 229 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 230 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 231 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 232 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 233 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 234 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 235 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 236 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 237 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 238 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 239 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 240 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 241 | - | k-next from:body -> { | |
| 242 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 243 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 244 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 245 | - | }; | |
| 246 | - | k-split from:body -> { | |
| 247 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 248 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 249 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 250 | - | }; | |
| 251 | - | k-unified from:body -> { | |
| 252 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 253 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 254 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 255 | - | }; | |
| 256 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 257 | - | k-permalink from:body -> { | |
| 258 | - | let b = document.body.dataset; | |
| 259 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 260 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 261 | - | navigator.clipboard?.writeText(location.href); | |
| 262 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 263 | - | }"> | |
| 197 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 198 | + | k-k from:body -> ermite.move(-1); | |
| 199 | + | k-top from:body -> ermite.edge(false); | |
| 200 | + | k-bottom from:body -> ermite.edge(true); | |
| 201 | + | k-open from:body -> ermite.open(); | |
| 202 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 203 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 204 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 205 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 206 | + | k-filter from:body -> ermite.focusFilter(); | |
| 207 | + | k-gh from:body -> ermite.go('.brand'); | |
| 208 | + | k-s from:body -> ermite.tab('s'); | |
| 209 | + | k-t from:body -> ermite.tab('t'); | |
| 210 | + | k-c from:body -> ermite.tab('c'); | |
| 211 | + | k-r from:body -> ermite.tab('r'); | |
| 212 | + | k-m from:body -> ermite.tab('m'); | |
| 213 | + | k-next from:body -> ermite.nextNote(); | |
| 214 | + | k-split from:body -> ermite.layout('split'); | |
| 215 | + | k-unified from:body -> ermite.layout('unified'); | |
| 216 | + | k-collapse from:body -> ermite.collapse(); | |
| 217 | + | k-permalink from:body -> ermite.permalink()"> | |
| 264 | 218 | </div> | |
| 265 | 219 | </body> | |
| 266 | 220 | </html> | |
M cmd/ermite/testdata/web/not-found.html +24 -70
| @@ -35,13 +35,13 @@ | |||
| 35 | 35 | </ol> | |
| 36 | 36 | </nav> | |
| 37 | 37 | <div class="hdr-acts"> | |
| 38 | - | <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 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 39 | 39 | <span class="ico i-search" aria-hidden="true"> | |
| 40 | 40 | </span> | |
| 41 | 41 | <span class="search-label">Go to…</span> | |
| 42 | 42 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 43 | 43 | </button> | |
| 44 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 44 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 45 | 45 | </div> | |
| 46 | 46 | </div> | |
| 47 | 47 | </header> | |
| @@ -171,13 +171,7 @@ | |||
| 171 | 171 | <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()"> | |
| 172 | 172 | <div class="panel-head"> | |
| 173 | 173 | <label for="cmd-q" class="sr">go to</label> | |
| 174 | - | <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(); | |
| 175 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 176 | - | 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(); | |
| 177 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 178 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 179 | - | 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' })) } | |
| 180 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 174 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 181 | 175 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 182 | 176 | <kbd>esc</kbd> | |
| 183 | 177 | </button> | |
| @@ -200,67 +194,27 @@ | |||
| 200 | 194 | <kbd>esc</kbd> close</span> | |
| 201 | 195 | </p> | |
| 202 | 196 | </dialog> | |
| 203 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 204 | - | if (q('dialog[open]').count) return; | |
| 205 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 206 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 207 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 208 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 209 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 210 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 211 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 212 | - | })(1); | |
| 213 | - | k-k from:body -> (async (d) => { | |
| 214 | - | if (q('dialog[open]').count) return; | |
| 215 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 216 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 217 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 218 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 219 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 220 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 221 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 222 | - | })(-1); | |
| 223 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 224 | - | 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)); | |
| 225 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 226 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 227 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 228 | - | 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()); | |
| 229 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 230 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 231 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 232 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 233 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 234 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 235 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 236 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 237 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 238 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 239 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 240 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 241 | - | k-next from:body -> { | |
| 242 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 243 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 244 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 245 | - | }; | |
| 246 | - | k-split from:body -> { | |
| 247 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 248 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 249 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 250 | - | }; | |
| 251 | - | k-unified from:body -> { | |
| 252 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 253 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 254 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 255 | - | }; | |
| 256 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 257 | - | k-permalink from:body -> { | |
| 258 | - | let b = document.body.dataset; | |
| 259 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 260 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 261 | - | navigator.clipboard?.writeText(location.href); | |
| 262 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 263 | - | }"> | |
| 197 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 198 | + | k-k from:body -> ermite.move(-1); | |
| 199 | + | k-top from:body -> ermite.edge(false); | |
| 200 | + | k-bottom from:body -> ermite.edge(true); | |
| 201 | + | k-open from:body -> ermite.open(); | |
| 202 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 203 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 204 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 205 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 206 | + | k-filter from:body -> ermite.focusFilter(); | |
| 207 | + | k-gh from:body -> ermite.go('.brand'); | |
| 208 | + | k-s from:body -> ermite.tab('s'); | |
| 209 | + | k-t from:body -> ermite.tab('t'); | |
| 210 | + | k-c from:body -> ermite.tab('c'); | |
| 211 | + | k-r from:body -> ermite.tab('r'); | |
| 212 | + | k-m from:body -> ermite.tab('m'); | |
| 213 | + | k-next from:body -> ermite.nextNote(); | |
| 214 | + | k-split from:body -> ermite.layout('split'); | |
| 215 | + | k-unified from:body -> ermite.layout('unified'); | |
| 216 | + | k-collapse from:body -> ermite.collapse(); | |
| 217 | + | k-permalink from:body -> ermite.permalink()"> | |
| 264 | 218 | </div> | |
| 265 | 219 | </body> | |
| 266 | 220 | </html> | |
M cmd/ermite/testdata/web/refs.html +30 -75
| @@ -38,19 +38,19 @@ | |||
| 38 | 38 | </ol> | |
| 39 | 39 | </nav> | |
| 40 | 40 | <div class="hdr-acts"> | |
| 41 | - | <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 | + | <a class="btn ref" href="/demo/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 42 | 42 | <span class="ico i-branch" aria-hidden="true"> | |
| 43 | 43 | </span> | |
| 44 | 44 | <span class="ref-name">main</span> | |
| 45 | 45 | <span class="sha">SHA7</span> | |
| 46 | - | </button> | |
| 47 | - | <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'))"> | |
| 46 | + | </a> | |
| 47 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 48 | 48 | <span class="ico i-search" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="search-label">Go to…</span> | |
| 51 | 51 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 52 | 52 | </button> | |
| 53 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 53 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 54 | 54 | </div> | |
| 55 | 55 | </div> | |
| 56 | 56 | <nav class="tabs" aria-label="Sections"> | |
| @@ -82,8 +82,7 @@ | |||
| 82 | 82 | <main id="main" tabindex="-1"> | |
| 83 | 83 | <div class="filter"> | |
| 84 | 84 | <label for="ref-filter" class="sr">filter</label> | |
| 85 | - | <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)); | |
| 86 | - | let first = q('#ref-rows > tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#ref-rows > tr')"> | |
| 85 | + | <input id="ref-filter" type="search" data-filter="#ref-rows > tr" placeholder="Filter branches and tags…" autocomplete="off" hx-on:input="ermite.filter(this)"> | |
| 87 | 86 | </div> | |
| 88 | 87 | <table class="list refs" aria-label="Branches and tags"> | |
| 89 | 88 | <thead> | |
| @@ -193,6 +192,8 @@ | |||
| 193 | 192 | </span> | |
| 194 | 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'; | |
| 195 | 194 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 195 | + | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 196 | + | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 196 | 197 | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 197 | 198 | gap -> ermiteLive.reload()"> | |
| 198 | 199 | </span> | |
| @@ -296,7 +297,7 @@ | |||
| 296 | 297 | <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()"> | |
| 297 | 298 | <div class="panel-head"> | |
| 298 | 299 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 299 | - | <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))"> | |
| 300 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 300 | 301 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 301 | 302 | <kbd>esc</kbd> | |
| 302 | 303 | </button> | |
| @@ -335,13 +336,7 @@ | |||
| 335 | 336 | <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()"> | |
| 336 | 337 | <div class="panel-head"> | |
| 337 | 338 | <label for="cmd-q" class="sr">go to</label> | |
| 338 | - | <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(); | |
| 339 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 340 | - | 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(); | |
| 341 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 342 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 343 | - | 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' })) } | |
| 344 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 339 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 345 | 340 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 346 | 341 | <kbd>esc</kbd> | |
| 347 | 342 | </button> | |
| @@ -424,67 +419,27 @@ | |||
| 424 | 419 | <kbd>esc</kbd> close</span> | |
| 425 | 420 | </p> | |
| 426 | 421 | </dialog> | |
| 427 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 428 | - | if (q('dialog[open]').count) return; | |
| 429 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 430 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 431 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 432 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 433 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 434 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 435 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 436 | - | })(1); | |
| 437 | - | k-k from:body -> (async (d) => { | |
| 438 | - | if (q('dialog[open]').count) return; | |
| 439 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 440 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 441 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 442 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 443 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 444 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 445 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 446 | - | })(-1); | |
| 447 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 448 | - | 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)); | |
| 449 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 450 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 451 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 452 | - | 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()); | |
| 453 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 454 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 455 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 456 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 457 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 458 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 459 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 460 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 461 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 462 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 463 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 464 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 465 | - | k-next from:body -> { | |
| 466 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 467 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 468 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 469 | - | }; | |
| 470 | - | k-split from:body -> { | |
| 471 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 472 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 473 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 474 | - | }; | |
| 475 | - | k-unified from:body -> { | |
| 476 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 477 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 478 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 479 | - | }; | |
| 480 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 481 | - | k-permalink from:body -> { | |
| 482 | - | let b = document.body.dataset; | |
| 483 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 484 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 485 | - | navigator.clipboard?.writeText(location.href); | |
| 486 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 487 | - | }"> | |
| 422 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 423 | + | k-k from:body -> ermite.move(-1); | |
| 424 | + | k-top from:body -> ermite.edge(false); | |
| 425 | + | k-bottom from:body -> ermite.edge(true); | |
| 426 | + | k-open from:body -> ermite.open(); | |
| 427 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 428 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 429 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 430 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 431 | + | k-filter from:body -> ermite.focusFilter(); | |
| 432 | + | k-gh from:body -> ermite.go('.brand'); | |
| 433 | + | k-s from:body -> ermite.tab('s'); | |
| 434 | + | k-t from:body -> ermite.tab('t'); | |
| 435 | + | k-c from:body -> ermite.tab('c'); | |
| 436 | + | k-r from:body -> ermite.tab('r'); | |
| 437 | + | k-m from:body -> ermite.tab('m'); | |
| 438 | + | k-next from:body -> ermite.nextNote(); | |
| 439 | + | k-split from:body -> ermite.layout('split'); | |
| 440 | + | k-unified from:body -> ermite.layout('unified'); | |
| 441 | + | k-collapse from:body -> ermite.collapse(); | |
| 442 | + | k-permalink from:body -> ermite.permalink()"> | |
| 488 | 443 | </div> | |
| 489 | 444 | </body> | |
| 490 | 445 | </html> | |
M cmd/ermite/testdata/web/summary.html +30 -74
| @@ -35,19 +35,19 @@ | |||
| 35 | 35 | </ol> | |
| 36 | 36 | </nav> | |
| 37 | 37 | <div class="hdr-acts"> | |
| 38 | - | <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 | + | <a class="btn ref" href="/demo/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 39 | 39 | <span class="ico i-branch" aria-hidden="true"> | |
| 40 | 40 | </span> | |
| 41 | 41 | <span class="ref-name">main</span> | |
| 42 | 42 | <span class="sha">SHA7</span> | |
| 43 | - | </button> | |
| 44 | - | <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'))"> | |
| 43 | + | </a> | |
| 44 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 45 | 45 | <span class="ico i-search" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="search-label">Go to…</span> | |
| 48 | 48 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 49 | 49 | </button> | |
| 50 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 50 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 51 | 51 | </div> | |
| 52 | 52 | </div> | |
| 53 | 53 | <nav class="tabs" aria-label="Sections"> | |
| @@ -117,7 +117,7 @@ | |||
| 117 | 117 | <div class="clone-line"> | |
| 118 | 118 | <span class="mute" aria-hidden="true">$ </span> | |
| 119 | 119 | <code>git clone http://example.test/demo.git</code> | |
| 120 | - | <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> | |
| 120 | + | <button type="button" class="btn copy" data-js data-label="Copy" aria-label="copy: git clone http://example.test/demo.git" hx-on:click="ermite.copy(this)">Copy</button> | |
| 121 | 121 | </div> | |
| 122 | 122 | </div> | |
| 123 | 123 | </section> | |
| @@ -211,6 +211,8 @@ | |||
| 211 | 211 | </span> | |
| 212 | 212 | <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'; | |
| 213 | 213 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 214 | + | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 215 | + | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 214 | 216 | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 215 | 217 | gap -> ermiteLive.reload()"> | |
| 216 | 218 | </span> | |
| @@ -314,7 +316,7 @@ | |||
| 314 | 316 | <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()"> | |
| 315 | 317 | <div class="panel-head"> | |
| 316 | 318 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 317 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 318 | 320 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 319 | 321 | <kbd>esc</kbd> | |
| 320 | 322 | </button> | |
| @@ -353,13 +355,7 @@ | |||
| 353 | 355 | <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()"> | |
| 354 | 356 | <div class="panel-head"> | |
| 355 | 357 | <label for="cmd-q" class="sr">go to</label> | |
| 356 | - | <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(); | |
| 357 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 358 | - | 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(); | |
| 359 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 360 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 361 | - | 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' })) } | |
| 362 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 358 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 363 | 359 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 364 | 360 | <kbd>esc</kbd> | |
| 365 | 361 | </button> | |
| @@ -442,67 +438,27 @@ | |||
| 442 | 438 | <kbd>esc</kbd> close</span> | |
| 443 | 439 | </p> | |
| 444 | 440 | </dialog> | |
| 445 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 446 | - | if (q('dialog[open]').count) return; | |
| 447 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 448 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 449 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 450 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 451 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 452 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 453 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 454 | - | })(1); | |
| 455 | - | k-k from:body -> (async (d) => { | |
| 456 | - | if (q('dialog[open]').count) return; | |
| 457 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 458 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 459 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 460 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 461 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 462 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 463 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 464 | - | })(-1); | |
| 465 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 466 | - | 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)); | |
| 467 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 468 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 469 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 470 | - | 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()); | |
| 471 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 472 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 473 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 474 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 475 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 476 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 477 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 478 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 479 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 480 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 481 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 482 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 483 | - | k-next from:body -> { | |
| 484 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 485 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 486 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 487 | - | }; | |
| 488 | - | k-split from:body -> { | |
| 489 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 490 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 491 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 492 | - | }; | |
| 493 | - | k-unified from:body -> { | |
| 494 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 495 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 496 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 497 | - | }; | |
| 498 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 499 | - | k-permalink from:body -> { | |
| 500 | - | let b = document.body.dataset; | |
| 501 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 502 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 503 | - | navigator.clipboard?.writeText(location.href); | |
| 504 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 505 | - | }"> | |
| 441 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 442 | + | k-k from:body -> ermite.move(-1); | |
| 443 | + | k-top from:body -> ermite.edge(false); | |
| 444 | + | k-bottom from:body -> ermite.edge(true); | |
| 445 | + | k-open from:body -> ermite.open(); | |
| 446 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 447 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 448 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 449 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 450 | + | k-filter from:body -> ermite.focusFilter(); | |
| 451 | + | k-gh from:body -> ermite.go('.brand'); | |
| 452 | + | k-s from:body -> ermite.tab('s'); | |
| 453 | + | k-t from:body -> ermite.tab('t'); | |
| 454 | + | k-c from:body -> ermite.tab('c'); | |
| 455 | + | k-r from:body -> ermite.tab('r'); | |
| 456 | + | k-m from:body -> ermite.tab('m'); | |
| 457 | + | k-next from:body -> ermite.nextNote(); | |
| 458 | + | k-split from:body -> ermite.layout('split'); | |
| 459 | + | k-unified from:body -> ermite.layout('unified'); | |
| 460 | + | k-collapse from:body -> ermite.collapse(); | |
| 461 | + | k-permalink from:body -> ermite.permalink()"> | |
| 506 | 462 | </div> | |
| 507 | 463 | </body> | |
| 508 | 464 | </html> | |
M cmd/ermite/testdata/web/tree-nested.html +28 -74
| @@ -47,19 +47,19 @@ | |||
| 47 | 47 | </ol> | |
| 48 | 48 | </nav> | |
| 49 | 49 | <div class="hdr-acts"> | |
| 50 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 51 | 51 | <span class="ico i-branch" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="ref-name">main</span> | |
| 54 | 54 | <span class="sha">SHA7</span> | |
| 55 | - | </button> | |
| 56 | - | <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'))"> | |
| 55 | + | </a> | |
| 56 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 57 | 57 | <span class="ico i-search" aria-hidden="true"> | |
| 58 | 58 | </span> | |
| 59 | 59 | <span class="search-label">Go to…</span> | |
| 60 | 60 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 61 | 61 | </button> | |
| 62 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 62 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 63 | 63 | </div> | |
| 64 | 64 | </div> | |
| 65 | 65 | <nav class="tabs" aria-label="Sections"> | |
| @@ -91,7 +91,7 @@ | |||
| 91 | 91 | <main id="main" tabindex="-1"> | |
| 92 | 92 | <div class="filter"> | |
| 93 | 93 | <label for="tree-filter" class="sr">filter</label> | |
| 94 | - | <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])')"> | |
| 94 | + | <input id="tree-filter" type="search" data-filter="#tree-rows > tr" placeholder="Filter this directory…" autocomplete="off" hx-on:input="ermite.filter(this)"> | |
| 95 | 95 | </div> | |
| 96 | 96 | <table class="list tree" aria-label="Entries"> | |
| 97 | 97 | <thead> | |
| @@ -241,7 +241,7 @@ | |||
| 241 | 241 | <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()"> | |
| 242 | 242 | <div class="panel-head"> | |
| 243 | 243 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 244 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 245 | 245 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 246 | 246 | <kbd>esc</kbd> | |
| 247 | 247 | </button> | |
| @@ -276,13 +276,7 @@ | |||
| 276 | 276 | <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()"> | |
| 277 | 277 | <div class="panel-head"> | |
| 278 | 278 | <label for="cmd-q" class="sr">go to</label> | |
| 279 | - | <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(); | |
| 280 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 281 | - | 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(); | |
| 282 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 283 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 284 | - | 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' })) } | |
| 285 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 279 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 286 | 280 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 287 | 281 | <kbd>esc</kbd> | |
| 288 | 282 | </button> | |
| @@ -359,67 +353,27 @@ | |||
| 359 | 353 | <kbd>esc</kbd> close</span> | |
| 360 | 354 | </p> | |
| 361 | 355 | </dialog> | |
| 362 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 363 | - | if (q('dialog[open]').count) return; | |
| 364 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 365 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 366 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 367 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 368 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 369 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 370 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 371 | - | })(1); | |
| 372 | - | k-k from:body -> (async (d) => { | |
| 373 | - | if (q('dialog[open]').count) return; | |
| 374 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 375 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 376 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 377 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 378 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 379 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 380 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 381 | - | })(-1); | |
| 382 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 383 | - | k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9)); | |
| 384 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 385 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 386 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 387 | - | 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()); | |
| 388 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 389 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 390 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 391 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 392 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 393 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 394 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 395 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 396 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 397 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 398 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 399 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 400 | - | k-next from:body -> { | |
| 401 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 402 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 403 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 404 | - | }; | |
| 405 | - | k-split from:body -> { | |
| 406 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 407 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 408 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 409 | - | }; | |
| 410 | - | k-unified from:body -> { | |
| 411 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 412 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 413 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 414 | - | }; | |
| 415 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 416 | - | k-permalink from:body -> { | |
| 417 | - | let b = document.body.dataset; | |
| 418 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 419 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 420 | - | navigator.clipboard?.writeText(location.href); | |
| 421 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 422 | - | }"> | |
| 356 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 357 | + | k-k from:body -> ermite.move(-1); | |
| 358 | + | k-top from:body -> ermite.edge(false); | |
| 359 | + | k-bottom from:body -> ermite.edge(true); | |
| 360 | + | k-open from:body -> ermite.open(); | |
| 361 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 362 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 363 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 364 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 365 | + | k-filter from:body -> ermite.focusFilter(); | |
| 366 | + | k-gh from:body -> ermite.go('.brand'); | |
| 367 | + | k-s from:body -> ermite.tab('s'); | |
| 368 | + | k-t from:body -> ermite.tab('t'); | |
| 369 | + | k-c from:body -> ermite.tab('c'); | |
| 370 | + | k-r from:body -> ermite.tab('r'); | |
| 371 | + | k-m from:body -> ermite.tab('m'); | |
| 372 | + | k-next from:body -> ermite.nextNote(); | |
| 373 | + | k-split from:body -> ermite.layout('split'); | |
| 374 | + | k-unified from:body -> ermite.layout('unified'); | |
| 375 | + | k-collapse from:body -> ermite.collapse(); | |
| 376 | + | k-permalink from:body -> ermite.permalink()"> | |
| 423 | 377 | </div> | |
| 424 | 378 | </body> | |
| 425 | 379 | </html> | |
M cmd/ermite/testdata/web/tree-root.html +28 -74
| @@ -41,19 +41,19 @@ | |||
| 41 | 41 | </ol> | |
| 42 | 42 | </nav> | |
| 43 | 43 | <div class="hdr-acts"> | |
| 44 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 45 | 45 | <span class="ico i-branch" aria-hidden="true"> | |
| 46 | 46 | </span> | |
| 47 | 47 | <span class="ref-name">main</span> | |
| 48 | 48 | <span class="sha">SHA7</span> | |
| 49 | - | </button> | |
| 50 | - | <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'))"> | |
| 49 | + | </a> | |
| 50 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 51 | 51 | <span class="ico i-search" aria-hidden="true"> | |
| 52 | 52 | </span> | |
| 53 | 53 | <span class="search-label">Go to…</span> | |
| 54 | 54 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 55 | 55 | </button> | |
| 56 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 56 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | </div> | |
| 59 | 59 | <nav class="tabs" aria-label="Sections"> | |
| @@ -85,7 +85,7 @@ | |||
| 85 | 85 | <main id="main" tabindex="-1"> | |
| 86 | 86 | <div class="filter"> | |
| 87 | 87 | <label for="tree-filter" class="sr">filter</label> | |
| 88 | - | <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])')"> | |
| 88 | + | <input id="tree-filter" type="search" data-filter="#tree-rows > tr" placeholder="Filter this directory…" autocomplete="off" hx-on:input="ermite.filter(this)"> | |
| 89 | 89 | </div> | |
| 90 | 90 | <table class="list tree" aria-label="Entries"> | |
| 91 | 91 | <thead> | |
| @@ -468,7 +468,7 @@ | |||
| 468 | 468 | <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()"> | |
| 469 | 469 | <div class="panel-head"> | |
| 470 | 470 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 471 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 472 | 472 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 473 | 473 | <kbd>esc</kbd> | |
| 474 | 474 | </button> | |
| @@ -503,13 +503,7 @@ | |||
| 503 | 503 | <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()"> | |
| 504 | 504 | <div class="panel-head"> | |
| 505 | 505 | <label for="cmd-q" class="sr">go to</label> | |
| 506 | - | <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(); | |
| 507 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 508 | - | 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(); | |
| 509 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 510 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 511 | - | 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' })) } | |
| 512 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 506 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 513 | 507 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 514 | 508 | <kbd>esc</kbd> | |
| 515 | 509 | </button> | |
| @@ -658,67 +652,27 @@ | |||
| 658 | 652 | <kbd>esc</kbd> close</span> | |
| 659 | 653 | </p> | |
| 660 | 654 | </dialog> | |
| 661 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 662 | - | if (q('dialog[open]').count) return; | |
| 663 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 664 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 665 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 666 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 667 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 668 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 669 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 670 | - | })(1); | |
| 671 | - | k-k from:body -> (async (d) => { | |
| 672 | - | if (q('dialog[open]').count) return; | |
| 673 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 674 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 675 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 676 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 677 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 678 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 679 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 680 | - | })(-1); | |
| 681 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 682 | - | 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)); | |
| 683 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 684 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 685 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 686 | - | 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()); | |
| 687 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 688 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 689 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 690 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 691 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 692 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 693 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 694 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 695 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 696 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 697 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 698 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 699 | - | k-next from:body -> { | |
| 700 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 701 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 702 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 703 | - | }; | |
| 704 | - | k-split from:body -> { | |
| 705 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 706 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 707 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 708 | - | }; | |
| 709 | - | k-unified from:body -> { | |
| 710 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 711 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 712 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 713 | - | }; | |
| 714 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 715 | - | k-permalink from:body -> { | |
| 716 | - | let b = document.body.dataset; | |
| 717 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 718 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 719 | - | navigator.clipboard?.writeText(location.href); | |
| 720 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 721 | - | }"> | |
| 655 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 656 | + | k-k from:body -> ermite.move(-1); | |
| 657 | + | k-top from:body -> ermite.edge(false); | |
| 658 | + | k-bottom from:body -> ermite.edge(true); | |
| 659 | + | k-open from:body -> ermite.open(); | |
| 660 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 661 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 662 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 663 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 664 | + | k-filter from:body -> ermite.focusFilter(); | |
| 665 | + | k-gh from:body -> ermite.go('.brand'); | |
| 666 | + | k-s from:body -> ermite.tab('s'); | |
| 667 | + | k-t from:body -> ermite.tab('t'); | |
| 668 | + | k-c from:body -> ermite.tab('c'); | |
| 669 | + | k-r from:body -> ermite.tab('r'); | |
| 670 | + | k-m from:body -> ermite.tab('m'); | |
| 671 | + | k-next from:body -> ermite.nextNote(); | |
| 672 | + | k-split from:body -> ermite.layout('split'); | |
| 673 | + | k-unified from:body -> ermite.layout('unified'); | |
| 674 | + | k-collapse from:body -> ermite.collapse(); | |
| 675 | + | k-permalink from:body -> ermite.permalink()"> | |
| 722 | 676 | </div> | |
| 723 | 677 | </body> | |
| 724 | 678 | </html> | |
M cmd/ermite/testdata/web/tree-slash-ref.html +28 -74
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">feature/x</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -88,7 +88,7 @@ | |||
| 88 | 88 | <main id="main" tabindex="-1"> | |
| 89 | 89 | <div class="filter"> | |
| 90 | 90 | <label for="tree-filter" class="sr">filter</label> | |
| 91 | - | <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])')"> | |
| 91 | + | <input id="tree-filter" type="search" data-filter="#tree-rows > tr" placeholder="Filter this directory…" autocomplete="off" hx-on:input="ermite.filter(this)"> | |
| 92 | 92 | </div> | |
| 93 | 93 | <table class="list tree" aria-label="Entries"> | |
| 94 | 94 | <thead> | |
| @@ -238,7 +238,7 @@ | |||
| 238 | 238 | <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()"> | |
| 239 | 239 | <div class="panel-head"> | |
| 240 | 240 | <label for="refs-filter" class="sr">filter branches and tags</label> | |
| 241 | - | <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 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 242 | 242 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 243 | 243 | <kbd>esc</kbd> | |
| 244 | 244 | </button> | |
| @@ -273,13 +273,7 @@ | |||
| 273 | 273 | <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()"> | |
| 274 | 274 | <div class="panel-head"> | |
| 275 | 275 | <label for="cmd-q" class="sr">go to</label> | |
| 276 | - | <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(); | |
| 277 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 278 | - | 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(); | |
| 279 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 280 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 281 | - | 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' })) } | |
| 282 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 276 | + | <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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 283 | 277 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 284 | 278 | <kbd>esc</kbd> | |
| 285 | 279 | </button> | |
| @@ -356,67 +350,27 @@ | |||
| 356 | 350 | <kbd>esc</kbd> close</span> | |
| 357 | 351 | </p> | |
| 358 | 352 | </dialog> | |
| 359 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 360 | - | if (q('dialog[open]').count) return; | |
| 361 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 362 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 363 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 364 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 365 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 366 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 367 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 368 | - | })(1); | |
| 369 | - | k-k from:body -> (async (d) => { | |
| 370 | - | if (q('dialog[open]').count) return; | |
| 371 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 372 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 373 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 374 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 375 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 376 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 377 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 378 | - | })(-1); | |
| 379 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 380 | - | 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)); | |
| 381 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 382 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 383 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 384 | - | 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()); | |
| 385 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 386 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 387 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 388 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 389 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 390 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 391 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 392 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 393 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 394 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 395 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 396 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 397 | - | k-next from:body -> { | |
| 398 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 399 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 400 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 401 | - | }; | |
| 402 | - | k-split from:body -> { | |
| 403 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 404 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 405 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 406 | - | }; | |
| 407 | - | k-unified from:body -> { | |
| 408 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 409 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 410 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 411 | - | }; | |
| 412 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 413 | - | k-permalink from:body -> { | |
| 414 | - | let b = document.body.dataset; | |
| 415 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 416 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 417 | - | navigator.clipboard?.writeText(location.href); | |
| 418 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 419 | - | }"> | |
| 353 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 354 | + | k-k from:body -> ermite.move(-1); | |
| 355 | + | k-top from:body -> ermite.edge(false); | |
| 356 | + | k-bottom from:body -> ermite.edge(true); | |
| 357 | + | k-open from:body -> ermite.open(); | |
| 358 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 359 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 360 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 361 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 362 | + | k-filter from:body -> ermite.focusFilter(); | |
| 363 | + | k-gh from:body -> ermite.go('.brand'); | |
| 364 | + | k-s from:body -> ermite.tab('s'); | |
| 365 | + | k-t from:body -> ermite.tab('t'); | |
| 366 | + | k-c from:body -> ermite.tab('c'); | |
| 367 | + | k-r from:body -> ermite.tab('r'); | |
| 368 | + | k-m from:body -> ermite.tab('m'); | |
| 369 | + | k-next from:body -> ermite.nextNote(); | |
| 370 | + | k-split from:body -> ermite.layout('split'); | |
| 371 | + | k-unified from:body -> ermite.layout('unified'); | |
| 372 | + | k-collapse from:body -> ermite.collapse(); | |
| 373 | + | k-permalink from:body -> ermite.permalink()"> | |
| 420 | 374 | </div> | |
| 421 | 375 | </body> | |
| 422 | 376 | </html> | |
M cmd/ermite/testdata/web/tree-subdir.html +28 -74
| @@ -44,19 +44,19 @@ | |||
| 44 | 44 | </ol> | |
| 45 | 45 | </nav> | |
| 46 | 46 | <div class="hdr-acts"> | |
| 47 | - | <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 | + | <a class="btn ref" href="/files/refs" data-fire="refs" aria-keyshortcuts="b" title="switch branch or tag (b)"> | |
| 48 | 48 | <span class="ico i-branch" aria-hidden="true"> | |
| 49 | 49 | </span> | |
| 50 | 50 | <span class="ref-name">main</span> | |
| 51 | 51 | <span class="sha">SHA7</span> | |
| 52 | - | </button> | |
| 53 | - | <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'))"> | |
| 52 | + | </a> | |
| 53 | + | <button type="button" class="btn search" data-js aria-haspopup="dialog" aria-controls="panel-cmd" aria-keyshortcuts="Control+K :" title="go to a section, ref, file or repo (ctrl k or :)" data-fire="cmd"> | |
| 54 | 54 | <span class="ico i-search" aria-hidden="true"> | |
| 55 | 55 | </span> | |
| 56 | 56 | <span class="search-label">Go to…</span> | |
| 57 | 57 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 58 | 58 | </button> | |
| 59 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 59 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 60 | 60 | </div> | |
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="tabs" aria-label="Sections"> | |
| @@ -88,7 +88,7 @@ | |||
| 88 | 88 | <main id="main" tabindex="-1"> | |
| 89 | 89 | <div class="filter"> | |
| 90 | 90 | <label for="tree-filter" class="sr">filter</label> | |
| 91 | - | <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])')"> | |
| 91 | + | <input id="tree-filter" type="search" data-filter="#tree-rows > tr" placeholder="Filter this directory…" autocomplete="off" hx-on:input="ermite.filter(this)"> | |
| 92 | 92 | </div> | |
| 93 | 93 | <table class="list tree" aria-label="Entries"> | |
| 94 | 94 | <thead> | |
| @@ -263,7 +263,7 @@ | |||
| 263 | 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()"> | |
| 264 | 264 | <div class="panel-head"> | |
| 265 | 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))"> | |
| 266 | + | <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)"> | |
| 267 | 267 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 268 | 268 | <kbd>esc</kbd> | |
| 269 | 269 | </button> | |
| @@ -298,13 +298,7 @@ | |||
| 298 | 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()"> | |
| 299 | 299 | <div class="panel-head"> | |
| 300 | 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(); | |
| 302 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 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(); | |
| 304 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 305 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 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' })) } | |
| 307 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }"> | |
| 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="ermite.cmdFilter(this)" hx-on:keydown="ermite.cmdKey(event)"> | |
| 308 | 302 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"> | |
| 309 | 303 | <kbd>esc</kbd> | |
| 310 | 304 | </button> | |
| @@ -387,67 +381,27 @@ | |||
| 387 | 381 | <kbd>esc</kbd> close</span> | |
| 388 | 382 | </p> | |
| 389 | 383 | </dialog> | |
| 390 | - | <div id="keys" hidden hx-on="k-j from:body -> (async (d) => { | |
| 391 | - | if (q('dialog[open]').count) return; | |
| 392 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 393 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 394 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 395 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 396 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 397 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 398 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 399 | - | })(1); | |
| 400 | - | k-k from:body -> (async (d) => { | |
| 401 | - | if (q('dialog[open]').count) return; | |
| 402 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 403 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 404 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 405 | - | let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j]; | |
| 406 | - | q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' }); | |
| 407 | - | let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark; | |
| 408 | - | await timeout('1500ms'); if (b.nav === mark) b.nav = '' | |
| 409 | - | })(-1); | |
| 410 | - | k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0)); | |
| 411 | - | 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)); | |
| 412 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 413 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 414 | - | k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal(); | |
| 415 | - | 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()); | |
| 416 | - | k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus(); | |
| 417 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 418 | - | k-gh from:body -> document.querySelector('.brand')?.click(); | |
| 419 | - | k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 420 | - | k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 421 | - | k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 422 | - | k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click(); | |
| 423 | - | k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click(); | |
| 424 | - | k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click(); | |
| 425 | - | k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click(); | |
| 426 | - | k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 427 | - | k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click(); | |
| 428 | - | k-next from:body -> { | |
| 429 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 430 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 431 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 432 | - | }; | |
| 433 | - | k-split from:body -> { | |
| 434 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 435 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 436 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 437 | - | }; | |
| 438 | - | k-unified from:body -> { | |
| 439 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 440 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 441 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 442 | - | }; | |
| 443 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 444 | - | k-permalink from:body -> { | |
| 445 | - | let b = document.body.dataset; | |
| 446 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 447 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 448 | - | navigator.clipboard?.writeText(location.href); | |
| 449 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 450 | - | }"> | |
| 384 | + | <div id="keys" hidden hx-on="k-j from:body -> ermite.move(1); | |
| 385 | + | k-k from:body -> ermite.move(-1); | |
| 386 | + | k-top from:body -> ermite.edge(false); | |
| 387 | + | k-bottom from:body -> ermite.edge(true); | |
| 388 | + | k-open from:body -> ermite.open(); | |
| 389 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 390 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 391 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 392 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 393 | + | k-filter from:body -> ermite.focusFilter(); | |
| 394 | + | k-gh from:body -> ermite.go('.brand'); | |
| 395 | + | k-s from:body -> ermite.tab('s'); | |
| 396 | + | k-t from:body -> ermite.tab('t'); | |
| 397 | + | k-c from:body -> ermite.tab('c'); | |
| 398 | + | k-r from:body -> ermite.tab('r'); | |
| 399 | + | k-m from:body -> ermite.tab('m'); | |
| 400 | + | k-next from:body -> ermite.nextNote(); | |
| 401 | + | k-split from:body -> ermite.layout('split'); | |
| 402 | + | k-unified from:body -> ermite.layout('unified'); | |
| 403 | + | k-collapse from:body -> ermite.collapse(); | |
| 404 | + | k-permalink from:body -> ermite.permalink()"> | |
| 451 | 405 | </div> | |
| 452 | 406 | </body> | |
| 453 | 407 | </html> | |
M cmd/ermite/web_events_test.go +1 -1
| @@ -162,7 +162,7 @@ func TestWebEventsRepoFragments(t *testing.T) { | |||
| 162 | 162 | refs := h.sse("topic=repo:x&view=refs", "") | |
| 163 | 163 | ||
| 164 | 164 | _, _, page := h.get("/x/commits/main") | |
| 165 | - | require.Contains(t, page, `<div id="commits-head">`, "the slot of the banner") | |
| 165 | + | require.Contains(t, page, `<div id="commits-head" class="live-slot" role="status" data-slot>`, "the slot of the banner, a status region that stays in the page") | |
| 166 | 166 | m := regexp.MustCompile(`hx-sse:connect="([^"]+)"`).FindStringSubmatch(page) | |
| 167 | 167 | require.Len(t, m, 2, "the commits page is live") | |
| 168 | 168 | url := html.UnescapeString(m[1]) | |
M cmd/ermite/web_mr_test.go +1 -1
| @@ -511,7 +511,7 @@ func TestWebMergeRequestEvents(t *testing.T) { | |||
| 511 | 511 | r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "Secret <title>").ok(t) | |
| 512 | 512 | ev := list.next(t, watchWait) | |
| 513 | 513 | assert.Equal(t, "mr-notice", ev.name) | |
| 514 | - | assert.Contains(t, ev.data, `<div id="mr-notice" class="live-banner" role="status">`) | |
| 514 | + | assert.Contains(t, ev.data, `<div id="mr-notice" class="live-slot" role="status" data-slot>`) | |
| 515 | 515 | assert.Contains(t, ev.data, `href="/proj/merge-requests"`) | |
| 516 | 516 | assert.NotContains(t, ev.data, "Secret") | |
| 517 | 517 | ||
M internal/adapters/web/ASSETS.md +1 -1
| @@ -8,7 +8,7 @@ content-hashed names. Nothing is fetched at runtime. | |||
| 8 | 8 | | `static/js/htmx.min.js` | htmx 4.0.0 (BSD Zero Clause, `static/js/htmx-LICENSE`) | `mockups/assets/htmx/htmx.min.js` of the design | `e484d9171a9db30a39c8f16e3d709d4137f3211c659f8e6125816635033d593f` | | |
| 9 | 9 | | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` | | |
| 10 | 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | |
| 11 | - | | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | | | |
| 11 | + | | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | | | |
| 12 | 12 | | `static/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 | 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 | 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` | | |
M internal/adapters/web/commits.templ +4 -5
| @@ -11,19 +11,18 @@ templ Commits(l Layout, p CommitsPage) { | |||
| 11 | 11 | <input | |
| 12 | 12 | id="commit-filter" | |
| 13 | 13 | type="search" | |
| 14 | - | data-filter | |
| 14 | + | data-filter=".days tr" | |
| 15 | + | data-groups=".days section" | |
| 15 | 16 | placeholder="Filter by subject, sha or author…" | |
| 16 | 17 | autocomplete="off" | |
| 17 | - | hx-on:input="let t = this.value.toLowerCase(); q('.days tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | |
| 18 | - | q('.days section').arr().forEach(s => s.hidden = !s.querySelector('tr:not([hidden])')); | |
| 19 | - | let first = q('.days tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.days tr')" | |
| 18 | + | hx-on:input="ermite.filter(this)" | |
| 20 | 19 | /> | |
| 21 | 20 | </div> | |
| 22 | 21 | if p.Path != "" { | |
| 23 | 22 | <p class="lede dim">history of <code>{ p.Path }</code> at { p.Ref }</p> | |
| 24 | 23 | } | |
| 25 | 24 | if p.Live { | |
| 26 | - | <div id="commits-head"></div> | |
| 25 | + | @LiveSlot("commits-head") | |
| 27 | 26 | } | |
| 28 | 27 | <div class="days"> | |
| 29 | 28 | for _, g := range p.Days { | |
M internal/adapters/web/commits_templ.go +47 -47 generated file: collapsed
M internal/adapters/web/components.templ +2 -1
| @@ -22,9 +22,10 @@ templ CloneBlock(c Clone) { | |||
| 22 | 22 | <button | |
| 23 | 23 | type="button" | |
| 24 | 24 | class="btn copy" | |
| 25 | + | data-js | |
| 25 | 26 | data-label="Copy" | |
| 26 | 27 | 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 | + | hx-on:click="ermite.copy(this)" | |
| 28 | 29 | >Copy</button> | |
| 29 | 30 | </div> | |
| 30 | 31 | } | |
M internal/adapters/web/components_templ.go +25 -25 generated file: collapsed
M internal/adapters/web/diff.templ +3 -4
| @@ -106,8 +106,8 @@ templ viewLink(p DiffView, view, label, key, href string) { | |||
| 106 | 106 | aria-current="true" | |
| 107 | 107 | } | |
| 108 | 108 | if p.Both { | |
| 109 | - | hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))" | |
| 110 | - | :aria-current={ "q('#diffs').attr['data-diff'] === '" + view + "' ? 'true' : false" } | |
| 109 | + | data-fire={ view } | |
| 110 | + | :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false" | |
| 111 | 111 | } | |
| 112 | 112 | >{ label }<kbd aria-hidden="true">{ key }</kbd></a> | |
| 113 | 113 | } | |
| @@ -115,7 +115,7 @@ templ viewLink(p DiffView, view, label, key, href string) { | |||
| 115 | 115 | // collapseButton folds every file of the diff, or opens them all again when | |
| 116 | 116 | // none is open; its label follows the state. | |
| 117 | 117 | templ collapseButton() { | |
| 118 | - | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))" class="btn"> | |
| 118 | + | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 119 | 119 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span><kbd aria-hidden="true">x</kbd> | |
| 120 | 120 | </button> | |
| 121 | 121 | } | |
| @@ -142,7 +142,6 @@ templ FileList(files []DiffFile) { | |||
| 142 | 142 | <li> | |
| 143 | 143 | <a | |
| 144 | 144 | href={ templ.SafeURL("#" + f.ID) } | |
| 145 | - | :aria-current={ "(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === '" + f.ID + "' ? 'true' : false })()" } | |
| 146 | 145 | hx-on:click="document.getElementById(this.hash.slice(1)).open = true" | |
| 147 | 146 | > | |
| 148 | 147 | <span class="st" title={ f.Status }>{ f.Status }</span> | |
M internal/adapters/web/diff_templ.go +199 -212 generated file: collapsed
M internal/adapters/web/events.go +4 -1
| @@ -51,7 +51,10 @@ import ( | |||
| 51 | 51 | // changed: the page fetches itself again (ermiteLive in ermite.js) | |
| 52 | 52 | // | |
| 53 | 53 | // The data of an event is one fragment, a top-level element with an id, one | |
| 54 | - | // "data:" line per line of HTML; the page swaps the element that has that id. | |
| 54 | + | // "data:" line per line of HTML; the page swaps the element that has that id | |
| 55 | + | // (ermiteLive.swap). The slots of the banners (commits-head, mr-notice) are | |
| 56 | + | // status regions that stay in the page: only their children are replaced, so | |
| 57 | + | // that a screen reader announces the banner. | |
| 55 | 58 | // An event that changes nothing on the page is still sent as an id alone, so | |
| 56 | 59 | // that a reconnection resumes after it. | |
| 57 | 60 | ||
M internal/adapters/web/fragments.go +1 -1
| @@ -312,7 +312,7 @@ func (l *liveStream) banner(ctx context.Context, p *pb.Pushed) ([]fragment, erro | |||
| 312 | 312 | text = "the branch was rewritten — reload" | |
| 313 | 313 | } | |
| 314 | 314 | } | |
| 315 | - | c := CommitsSlot() | |
| 315 | + | c := LiveSlot("commits-head") | |
| 316 | 316 | if text != "" { | |
| 317 | 317 | c = CommitsBanner(text, repoHref(l.repo, "commits", l.ref)) | |
| 318 | 318 | } | |
M internal/adapters/web/fragments.templ +22 -11
| @@ -55,31 +55,40 @@ templ RefsKV(p RefsPage) { | |||
| 55 | 55 | </dl> | |
| 56 | 56 | } | |
| 57 | 57 | ||
| 58 | - | // CommitsSlot is the place of the banner of the commits page, empty. | |
| 59 | - | templ CommitsSlot() { | |
| 60 | - | <div id="commits-head"></div> | |
| 58 | + | // LiveSlot is the place of a live banner, empty: a status region that stays | |
| 59 | + | // in the page. The live updates (ermiteLive in ermite.js) replace its children, | |
| 60 | + | // never the element, so that a screen reader announces the new banner (a | |
| 61 | + | // region that is inserted with its content is not announced). | |
| 62 | + | templ LiveSlot(id string) { | |
| 63 | + | <div id={ id } class="live-slot" role="status" data-slot></div> | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // liveBanner is the slot with its banner: a link that reloads or opens the | |
| 67 | + | // page. Only the children of the element reach the page that has the slot. | |
| 68 | + | templ liveBanner(id, text, href string) { | |
| 69 | + | <div id={ id } class="live-slot" role="status" data-slot><a href={ templ.SafeURL(href) }>{ text }</a></div> | |
| 61 | 70 | } | |
| 62 | 71 | ||
| 63 | 72 | // CommitsBanner announces new commits on the branch the page lists. It links | |
| 64 | 73 | // to the first page of the branch. | |
| 65 | 74 | templ CommitsBanner(text, href string) { | |
| 66 | - | <div id="commits-head" class="live-banner" role="status"> | |
| 67 | - | <a href={ templ.SafeURL(href) }>{ text }</a> | |
| 68 | - | </div> | |
| 75 | + | @liveBanner("commits-head", text, href) | |
| 69 | 76 | } | |
| 70 | 77 | ||
| 71 | - | // MRNotice tells a merge request page that it is out of date. The empty slot | |
| 72 | - | // of the page has the same id, so the first notice replaces it. | |
| 78 | + | // MRNotice tells a merge request page that it is out of date, or the list that | |
| 79 | + | // its content changed. | |
| 73 | 80 | templ MRNotice(text, href string) { | |
| 74 | - | <div id="mr-notice" class="live-banner" role="status"> | |
| 75 | - | <a href={ templ.SafeURL(href) }>{ text }</a> | |
| 76 | - | </div> | |
| 81 | + | @liveBanner("mr-notice", text, href) | |
| 77 | 82 | } | |
| 78 | 83 | ||
| 79 | 84 | // Live is the connection to the event stream, in the hint badge. It shows | |
| 80 | 85 | // only once a connection was lost: a page that never connected, or that is | |
| 81 | 86 | // live, says nothing. The events of the stream are fragments that ermiteLive | |
| 82 | 87 | // (ermite.js) swaps in the page. A page without a stream renders nothing. | |
| 88 | + | // | |
| 89 | + | // hx-sse retries a lost connection for ever, with a growing delay. A refusal | |
| 90 | + | // that will not change (404: the repository is gone or hidden; 401) is handled | |
| 91 | + | // by ermiteLive.error, which has the page fetch itself again. | |
| 83 | 92 | templ Live(l Layout) { | |
| 84 | 93 | if l.Live && l.LiveURL != "" { | |
| 85 | 94 | <span | |
| @@ -90,6 +99,8 @@ templ Live(l Layout) { | |||
| 90 | 99 | :text="data.state === 'offline' ? 'live updates paused' : ''" | |
| 91 | 100 | hx-on="htmx:sse:after:connection -> data.state = 'live'; | |
| 92 | 101 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 102 | + | htmx:sse:error -> ermiteLive.error(event.detail.status); | |
| 103 | + | htmx:sse:before:connection -> ermiteLive.gate(event); | |
| 93 | 104 | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 94 | 105 | gap -> ermiteLive.reload()" | |
| 95 | 106 | ></span> | |
M internal/adapters/web/fragments_templ.go +94 -56 generated file: collapsed
A internal/adapters/web/fragments_test.go +74 -0
| @@ -0,0 +1,74 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | "github.com/a-h/templ" | |
| 11 | + | "github.com/stretchr/testify/assert" | |
| 12 | + | "github.com/stretchr/testify/require" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | func renderToString(t *testing.T, c templ.Component) string { | |
| 16 | + | t.Helper() | |
| 17 | + | var b bytes.Buffer | |
| 18 | + | require.NoError(t, c.Render(t.Context(), &b)) | |
| 19 | + | return b.String() | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | // A live banner is the content of a status region that stays in the page: the | |
| 23 | + | // empty slot of the page and the fragment of the stream are the same element | |
| 24 | + | // (same id, role and marker), so that ermiteLive can replace the children and | |
| 25 | + | // leave the region where a screen reader watches it. | |
| 26 | + | func TestLiveSlotsAreStableStatusRegions(t *testing.T) { | |
| 27 | + | for _, tt := range []struct { | |
| 28 | + | name string | |
| 29 | + | c templ.Component | |
| 30 | + | id string | |
| 31 | + | text string | |
| 32 | + | }{ | |
| 33 | + | {"empty commits slot", LiveSlot("commits-head"), "commits-head", ""}, | |
| 34 | + | {"empty mr slot", LiveSlot("mr-notice"), "mr-notice", ""}, | |
| 35 | + | {"commits banner", CommitsBanner("2 new commits — reload", "/x/commits/main"), "commits-head", "2 new commits — reload"}, | |
| 36 | + | {"mr notice", MRNotice("a comment was added — reload", "/x/merge-requests/1"), "mr-notice", "a comment was added — reload"}, | |
| 37 | + | } { | |
| 38 | + | t.Run(tt.name, func(t *testing.T) { | |
| 39 | + | got := renderToString(t, tt.c) | |
| 40 | + | assert.True(t, strings.HasPrefix(got, `<div id="`+tt.id+`" class="live-slot" role="status" data-slot>`), got) | |
| 41 | + | if tt.text == "" { | |
| 42 | + | assert.True(t, strings.HasSuffix(got, "data-slot></div>"), "an empty slot has no child: %s", got) | |
| 43 | + | } else { | |
| 44 | + | assert.Contains(t, got, tt.text) | |
| 45 | + | } | |
| 46 | + | assert.True(t, strings.HasSuffix(got, "</div>"), got) | |
| 47 | + | }) | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // The header's refs button is a link to the refs page, which the script turns | |
| 52 | + | // into the dialog; the filters and the buttons that need scripts are marked. | |
| 53 | + | func TestShellDegradesWithoutScripts(t *testing.T) { | |
| 54 | + | l := Layout{Repo: "demo", Ref: "main", Short: "abc1234", Assets: Assets{}} | |
| 55 | + | got := renderToString(t, Header(l)) | |
| 56 | + | assert.Contains(t, got, `<a class="btn ref" href="/demo/refs" data-fire="refs"`) | |
| 57 | + | assert.NotContains(t, got, "hx-on:click", "no handler is inlined in the header") | |
| 58 | + | for _, want := range []string{`class="btn search" data-js`, `class="btn icon" data-js data-fire="help"`} { | |
| 59 | + | assert.Contains(t, got, want) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // Data reaches the scripts through data-* attributes, never spliced into an | |
| 64 | + | // attribute that is evaluated. | |
| 65 | + | func TestDiffControlsPassDataThroughAttributes(t *testing.T) { | |
| 66 | + | files := renderToString(t, FileList([]DiffFile{{ID: "f1'x", Path: "a.go", Status: "M"}})) | |
| 67 | + | assert.NotContains(t, files, ":aria-current", "the file in view is marked by ermite.js") | |
| 68 | + | assert.NotContains(t, files, "f1'x'", "no id is spliced into a script") | |
| 69 | + | ||
| 70 | + | link := renderToString(t, viewLink(DiffView{Both: true, View: "split"}, "split", "Split", "d", "?view=split")) | |
| 71 | + | assert.Contains(t, link, `data-fire="split"`) | |
| 72 | + | assert.Contains(t, link, `this.dataset.view`) | |
| 73 | + | assert.NotContains(t, link, "=== 'split'") | |
| 74 | + | } | |
M internal/adapters/web/keys.templ +23 -63
| @@ -3,72 +3,32 @@ | |||
| 3 | 3 | package web | |
| 4 | 4 | ||
| 5 | 5 | // Keys is the keyboard map. ermite.js turns keys into k-<name> events on | |
| 6 | - | // <body>; this element (hx-on + hx-live helpers) is the only place that | |
| 7 | - | // reacts to them. | |
| 6 | + | // <body>; this element is the only place that binds them, each to one call of | |
| 7 | + | // window.ermite (ermite.js), which does the work. | |
| 8 | 8 | templ Keys() { | |
| 9 | 9 | <div | |
| 10 | 10 | id="keys" | |
| 11 | 11 | hidden | |
| 12 | - | hx-on="k-j from:body -> (async (d) => { | |
| 13 | - | if (q('dialog[open]').count) return; | |
| 14 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 15 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 16 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 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 = '' | |
| 21 | - | })(1); | |
| 22 | - | k-k from:body -> (async (d) => { | |
| 23 | - | if (q('dialog[open]').count) return; | |
| 24 | - | let rows = q('[data-rows] > :not([hidden])').arr(); | |
| 25 | - | if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180)); | |
| 26 | - | let i = rows.findIndex(r => r.hasAttribute('data-cursor')); | |
| 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 = '' | |
| 31 | - | })(-1); | |
| 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)); | |
| 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)); | |
| 34 | - | k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click(); | |
| 35 | - | k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close()); | |
| 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(); | |
| 39 | - | k-filter from:body -> q('[data-filter]').arr()[0]?.focus(); | |
| 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(); | |
| 50 | - | k-next from:body -> { | |
| 51 | - | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 52 | - | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 53 | - | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 54 | - | }; | |
| 55 | - | k-split from:body -> { | |
| 56 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 57 | - | el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 58 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 59 | - | }; | |
| 60 | - | k-unified from:body -> { | |
| 61 | - | let el = q('[data-diff]').arr()[0]; if (!el) return; | |
| 62 | - | el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 63 | - | q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true) | |
| 64 | - | }; | |
| 65 | - | k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) }; | |
| 66 | - | k-permalink from:body -> { | |
| 67 | - | let b = document.body.dataset; | |
| 68 | - | if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return } | |
| 69 | - | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 70 | - | navigator.clipboard?.writeText(location.href); | |
| 71 | - | b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = '' | |
| 72 | - | }" | |
| 12 | + | hx-on="k-j from:body -> ermite.move(1); | |
| 13 | + | k-k from:body -> ermite.move(-1); | |
| 14 | + | k-top from:body -> ermite.edge(false); | |
| 15 | + | k-bottom from:body -> ermite.edge(true); | |
| 16 | + | k-open from:body -> ermite.open(); | |
| 17 | + | k-esc from:body -> ermite.closeDialogs(); | |
| 18 | + | k-help from:body -> ermite.dialog('panel-help'); | |
| 19 | + | k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter'); | |
| 20 | + | k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); | |
| 21 | + | k-filter from:body -> ermite.focusFilter(); | |
| 22 | + | k-gh from:body -> ermite.go('.brand'); | |
| 23 | + | k-s from:body -> ermite.tab('s'); | |
| 24 | + | k-t from:body -> ermite.tab('t'); | |
| 25 | + | k-c from:body -> ermite.tab('c'); | |
| 26 | + | k-r from:body -> ermite.tab('r'); | |
| 27 | + | k-m from:body -> ermite.tab('m'); | |
| 28 | + | k-next from:body -> ermite.nextNote(); | |
| 29 | + | k-split from:body -> ermite.layout('split'); | |
| 30 | + | k-unified from:body -> ermite.layout('unified'); | |
| 31 | + | k-collapse from:body -> ermite.collapse(); | |
| 32 | + | k-permalink from:body -> ermite.permalink()" | |
| 73 | 33 | ></div> | |
| 74 | 34 | } | |
M internal/adapters/web/keys_templ.go +3 -3 generated file: collapsed
M internal/adapters/web/mr.templ +4 -5
| @@ -9,18 +9,17 @@ import "strings" | |||
| 9 | 9 | templ MRList(l Layout, p MRListPage) { | |
| 10 | 10 | @Shell(l, mrListRail(p)) { | |
| 11 | 11 | if p.Live { | |
| 12 | - | <div id="mr-notice"></div> | |
| 12 | + | @LiveSlot("mr-notice") | |
| 13 | 13 | } | |
| 14 | 14 | <div class="filter"> | |
| 15 | 15 | <label for="mr-filter" class="sr">filter</label> | |
| 16 | 16 | <input | |
| 17 | 17 | id="mr-filter" | |
| 18 | 18 | type="search" | |
| 19 | - | data-filter | |
| 19 | + | data-filter=".mrs tbody tr" | |
| 20 | 20 | placeholder="Filter by title, branch, author or state…" | |
| 21 | 21 | autocomplete="off" | |
| 22 | - | hx-on:input="let t = this.value.toLowerCase(); q('.mrs tbody tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | |
| 23 | - | let first = q('.mrs tbody tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.mrs tbody tr')" | |
| 22 | + | hx-on:input="ermite.filter(this)" | |
| 24 | 23 | /> | |
| 25 | 24 | </div> | |
| 26 | 25 | if p.Newest == "" { | |
| @@ -140,7 +139,7 @@ templ MergeRequest(l Layout, p MRPage) { | |||
| 140 | 139 | } | |
| 141 | 140 | </dl> | |
| 142 | 141 | </header> | |
| 143 | - | <div id="mr-notice"></div> | |
| 142 | + | @LiveSlot("mr-notice") | |
| 144 | 143 | if len(p.Desc) > 0 { | |
| 145 | 144 | <p class="mr-desc">@userText(p.Desc)</p> | |
| 146 | 145 | } | |
M internal/adapters/web/mr_templ.go +202 -194
| @@ -48,30 +48,30 @@ func MRList(l Layout, p MRListPage) templ.Component { | |||
| 48 | 48 | } | |
| 49 | 49 | ctx = templ.InitializeContext(ctx) | |
| 50 | 50 | if p.Live { | |
| 51 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"mr-notice\"></div>") | |
| 51 | + | templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer) | |
| 52 | 52 | if templ_7745c5c3_Err != nil { | |
| 53 | 53 | return templ_7745c5c3_Err | |
| 54 | 54 | } | |
| 55 | 55 | } | |
| 56 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div class=\"filter\"><label for=\"mr-filter\" class=\"sr\">filter</label> <input id=\"mr-filter\" type=\"search\" data-filter placeholder=\"Filter by title, branch, author or state…\" autocomplete=\"off\" hx-on:input=\"let t = this.value.toLowerCase(); q('.mrs tbody tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t));\n let first = q('.mrs tbody tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.mrs tbody tr')\"></div>") | |
| 56 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " <div class=\"filter\"><label for=\"mr-filter\" class=\"sr\">filter</label> <input id=\"mr-filter\" type=\"search\" data-filter=\".mrs tbody tr\" placeholder=\"Filter by title, branch, author or state…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div>") | |
| 57 | 57 | if templ_7745c5c3_Err != nil { | |
| 58 | 58 | return templ_7745c5c3_Err | |
| 59 | 59 | } | |
| 60 | 60 | if p.Newest == "" { | |
| 61 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<h2 class=\"rule-h\"><span>Open</span><span class=\"count\">") | |
| 61 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"rule-h\"><span>Open</span><span class=\"count\">") | |
| 62 | 62 | if templ_7745c5c3_Err != nil { | |
| 63 | 63 | return templ_7745c5c3_Err | |
| 64 | 64 | } | |
| 65 | 65 | var templ_7745c5c3_Var3 string | |
| 66 | 66 | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Open))) | |
| 67 | 67 | if templ_7745c5c3_Err != nil { | |
| 68 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 27, Col: 78} | |
| 68 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 26, Col: 78} | |
| 69 | 69 | } | |
| 70 | 70 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) | |
| 71 | 71 | if templ_7745c5c3_Err != nil { | |
| 72 | 72 | return templ_7745c5c3_Err | |
| 73 | 73 | } | |
| 74 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span></h2>") | |
| 74 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span></h2>") | |
| 75 | 75 | if templ_7745c5c3_Err != nil { | |
| 76 | 76 | return templ_7745c5c3_Err | |
| 77 | 77 | } | |
| @@ -79,54 +79,54 @@ func MRList(l Layout, p MRListPage) templ.Component { | |||
| 79 | 79 | if templ_7745c5c3_Err != nil { | |
| 80 | 80 | return templ_7745c5c3_Err | |
| 81 | 81 | } | |
| 82 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " ") | |
| 82 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ") | |
| 83 | 83 | if templ_7745c5c3_Err != nil { | |
| 84 | 84 | return templ_7745c5c3_Err | |
| 85 | 85 | } | |
| 86 | 86 | if len(p.Open) == 0 { | |
| 87 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p class=\"lede dim\">no open merge requests.</p>") | |
| 87 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"lede dim\">no open merge requests.</p>") | |
| 88 | 88 | if templ_7745c5c3_Err != nil { | |
| 89 | 89 | return templ_7745c5c3_Err | |
| 90 | 90 | } | |
| 91 | 91 | } | |
| 92 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ") | |
| 92 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ") | |
| 93 | 93 | if templ_7745c5c3_Err != nil { | |
| 94 | 94 | return templ_7745c5c3_Err | |
| 95 | 95 | } | |
| 96 | 96 | if p.OpenNote != "" { | |
| 97 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"note\">") | |
| 97 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"note\">") | |
| 98 | 98 | if templ_7745c5c3_Err != nil { | |
| 99 | 99 | return templ_7745c5c3_Err | |
| 100 | 100 | } | |
| 101 | 101 | var templ_7745c5c3_Var4 string | |
| 102 | 102 | templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(p.OpenNote) | |
| 103 | 103 | if templ_7745c5c3_Err != nil { | |
| 104 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 33, Col: 32} | |
| 104 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 32, Col: 32} | |
| 105 | 105 | } | |
| 106 | 106 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) | |
| 107 | 107 | if templ_7745c5c3_Err != nil { | |
| 108 | 108 | return templ_7745c5c3_Err | |
| 109 | 109 | } | |
| 110 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</p>") | |
| 110 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p>") | |
| 111 | 111 | if templ_7745c5c3_Err != nil { | |
| 112 | 112 | return templ_7745c5c3_Err | |
| 113 | 113 | } | |
| 114 | 114 | } | |
| 115 | 115 | } | |
| 116 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " <h2 class=\"rule-h\"><span>Merged and closed</span><span class=\"count\">") | |
| 116 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " <h2 class=\"rule-h\"><span>Merged and closed</span><span class=\"count\">") | |
| 117 | 117 | if templ_7745c5c3_Err != nil { | |
| 118 | 118 | return templ_7745c5c3_Err | |
| 119 | 119 | } | |
| 120 | 120 | var templ_7745c5c3_Var5 string | |
| 121 | 121 | templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Done))) | |
| 122 | 122 | if templ_7745c5c3_Err != nil { | |
| 123 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 36, Col: 90} | |
| 123 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 35, Col: 90} | |
| 124 | 124 | } | |
| 125 | 125 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) | |
| 126 | 126 | if templ_7745c5c3_Err != nil { | |
| 127 | 127 | return templ_7745c5c3_Err | |
| 128 | 128 | } | |
| 129 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></h2>") | |
| 129 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span></h2>") | |
| 130 | 130 | if templ_7745c5c3_Err != nil { | |
| 131 | 131 | return templ_7745c5c3_Err | |
| 132 | 132 | } | |
| @@ -134,74 +134,74 @@ func MRList(l Layout, p MRListPage) templ.Component { | |||
| 134 | 134 | if templ_7745c5c3_Err != nil { | |
| 135 | 135 | return templ_7745c5c3_Err | |
| 136 | 136 | } | |
| 137 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ") | |
| 137 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ") | |
| 138 | 138 | if templ_7745c5c3_Err != nil { | |
| 139 | 139 | return templ_7745c5c3_Err | |
| 140 | 140 | } | |
| 141 | 141 | if len(p.Done) == 0 { | |
| 142 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<p class=\"lede dim\">nothing merged or closed yet.</p>") | |
| 142 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<p class=\"lede dim\">nothing merged or closed yet.</p>") | |
| 143 | 143 | if templ_7745c5c3_Err != nil { | |
| 144 | 144 | return templ_7745c5c3_Err | |
| 145 | 145 | } | |
| 146 | 146 | } | |
| 147 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ") | |
| 147 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") | |
| 148 | 148 | if templ_7745c5c3_Err != nil { | |
| 149 | 149 | return templ_7745c5c3_Err | |
| 150 | 150 | } | |
| 151 | 151 | if p.Older != "" || p.Newest != "" { | |
| 152 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<nav class=\"pager\" aria-label=\"Pages\">") | |
| 152 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<nav class=\"pager\" aria-label=\"Pages\">") | |
| 153 | 153 | if templ_7745c5c3_Err != nil { | |
| 154 | 154 | return templ_7745c5c3_Err | |
| 155 | 155 | } | |
| 156 | 156 | if p.Newest != "" { | |
| 157 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<a class=\"btn\" href=\"") | |
| 157 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<a class=\"btn\" href=\"") | |
| 158 | 158 | if templ_7745c5c3_Err != nil { | |
| 159 | 159 | return templ_7745c5c3_Err | |
| 160 | 160 | } | |
| 161 | 161 | var templ_7745c5c3_Var6 templ.SafeURL | |
| 162 | 162 | templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Newest)) | |
| 163 | 163 | if templ_7745c5c3_Err != nil { | |
| 164 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 44, Col: 50} | |
| 164 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 43, Col: 50} | |
| 165 | 165 | } | |
| 166 | 166 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) | |
| 167 | 167 | if templ_7745c5c3_Err != nil { | |
| 168 | 168 | return templ_7745c5c3_Err | |
| 169 | 169 | } | |
| 170 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">← Newest</a> ") | |
| 170 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">← Newest</a> ") | |
| 171 | 171 | if templ_7745c5c3_Err != nil { | |
| 172 | 172 | return templ_7745c5c3_Err | |
| 173 | 173 | } | |
| 174 | 174 | } else { | |
| 175 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"btn\" aria-disabled=\"true\">← Newest</span> ") | |
| 175 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"btn\" aria-disabled=\"true\">← Newest</span> ") | |
| 176 | 176 | if templ_7745c5c3_Err != nil { | |
| 177 | 177 | return templ_7745c5c3_Err | |
| 178 | 178 | } | |
| 179 | 179 | } | |
| 180 | 180 | if p.Older != "" { | |
| 181 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<a class=\"btn\" href=\"") | |
| 181 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<a class=\"btn\" href=\"") | |
| 182 | 182 | if templ_7745c5c3_Err != nil { | |
| 183 | 183 | return templ_7745c5c3_Err | |
| 184 | 184 | } | |
| 185 | 185 | var templ_7745c5c3_Var7 templ.SafeURL | |
| 186 | 186 | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Older)) | |
| 187 | 187 | if templ_7745c5c3_Err != nil { | |
| 188 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 49, Col: 49} | |
| 188 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 48, Col: 49} | |
| 189 | 189 | } | |
| 190 | 190 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) | |
| 191 | 191 | if templ_7745c5c3_Err != nil { | |
| 192 | 192 | return templ_7745c5c3_Err | |
| 193 | 193 | } | |
| 194 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" rel=\"next\">Older →</a>") | |
| 194 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" rel=\"next\">Older →</a>") | |
| 195 | 195 | if templ_7745c5c3_Err != nil { | |
| 196 | 196 | return templ_7745c5c3_Err | |
| 197 | 197 | } | |
| 198 | 198 | } else { | |
| 199 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"btn\" aria-disabled=\"true\">Older →</span>") | |
| 199 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span class=\"btn\" aria-disabled=\"true\">Older →</span>") | |
| 200 | 200 | if templ_7745c5c3_Err != nil { | |
| 201 | 201 | return templ_7745c5c3_Err | |
| 202 | 202 | } | |
| 203 | 203 | } | |
| 204 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</nav>") | |
| 204 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</nav>") | |
| 205 | 205 | if templ_7745c5c3_Err != nil { | |
| 206 | 206 | return templ_7745c5c3_Err | |
| 207 | 207 | } | |
| @@ -238,180 +238,180 @@ func mrRows(rows []MRRow, label string) templ.Component { | |||
| 238 | 238 | } | |
| 239 | 239 | ctx = templ.ClearChildren(ctx) | |
| 240 | 240 | if len(rows) > 0 { | |
| 241 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<table class=\"list mrs\" aria-label=\"") | |
| 241 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<table class=\"list mrs\" aria-label=\"") | |
| 242 | 242 | if templ_7745c5c3_Err != nil { | |
| 243 | 243 | return templ_7745c5c3_Err | |
| 244 | 244 | } | |
| 245 | 245 | var templ_7745c5c3_Var9 string | |
| 246 | 246 | templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(label) | |
| 247 | 247 | if templ_7745c5c3_Err != nil { | |
| 248 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 60, Col: 44} | |
| 248 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 59, Col: 44} | |
| 249 | 249 | } | |
| 250 | 250 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) | |
| 251 | 251 | if templ_7745c5c3_Err != nil { | |
| 252 | 252 | return templ_7745c5c3_Err | |
| 253 | 253 | } | |
| 254 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><tbody data-rows>") | |
| 254 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"><tbody data-rows>") | |
| 255 | 255 | if templ_7745c5c3_Err != nil { | |
| 256 | 256 | return templ_7745c5c3_Err | |
| 257 | 257 | } | |
| 258 | 258 | for _, m := range rows { | |
| 259 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<tr") | |
| 259 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<tr") | |
| 260 | 260 | if templ_7745c5c3_Err != nil { | |
| 261 | 261 | return templ_7745c5c3_Err | |
| 262 | 262 | } | |
| 263 | 263 | if m.Cursor { | |
| 264 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " data-cursor") | |
| 264 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " data-cursor") | |
| 265 | 265 | if templ_7745c5c3_Err != nil { | |
| 266 | 266 | return templ_7745c5c3_Err | |
| 267 | 267 | } | |
| 268 | 268 | } | |
| 269 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " data-state=\"") | |
| 269 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " data-state=\"") | |
| 270 | 270 | if templ_7745c5c3_Err != nil { | |
| 271 | 271 | return templ_7745c5c3_Err | |
| 272 | 272 | } | |
| 273 | 273 | var templ_7745c5c3_Var10 string | |
| 274 | 274 | templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.State) | |
| 275 | 275 | if templ_7745c5c3_Err != nil { | |
| 276 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 63, Col: 55} | |
| 276 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 62, Col: 55} | |
| 277 | 277 | } | |
| 278 | 278 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) | |
| 279 | 279 | if templ_7745c5c3_Err != nil { | |
| 280 | 280 | return templ_7745c5c3_Err | |
| 281 | 281 | } | |
| 282 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\"><td class=\"cur\" aria-hidden=\"true\"></td><td class=\"c-n mute\">") | |
| 282 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"><td class=\"cur\" aria-hidden=\"true\"></td><td class=\"c-n mute\">") | |
| 283 | 283 | if templ_7745c5c3_Err != nil { | |
| 284 | 284 | return templ_7745c5c3_Err | |
| 285 | 285 | } | |
| 286 | 286 | var templ_7745c5c3_Var11 string | |
| 287 | 287 | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(m.Number) | |
| 288 | 288 | if templ_7745c5c3_Err != nil { | |
| 289 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 65, Col: 37} | |
| 289 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 64, Col: 37} | |
| 290 | 290 | } | |
| 291 | 291 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | |
| 292 | 292 | if templ_7745c5c3_Err != nil { | |
| 293 | 293 | return templ_7745c5c3_Err | |
| 294 | 294 | } | |
| 295 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td><td class=\"c-state\">") | |
| 295 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</td><td class=\"c-state\">") | |
| 296 | 296 | if templ_7745c5c3_Err != nil { | |
| 297 | 297 | return templ_7745c5c3_Err | |
| 298 | 298 | } | |
| 299 | 299 | var templ_7745c5c3_Var12 string | |
| 300 | 300 | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(m.State) | |
| 301 | 301 | if templ_7745c5c3_Err != nil { | |
| 302 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 66, Col: 35} | |
| 302 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 65, Col: 35} | |
| 303 | 303 | } | |
| 304 | 304 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) | |
| 305 | 305 | if templ_7745c5c3_Err != nil { | |
| 306 | 306 | return templ_7745c5c3_Err | |
| 307 | 307 | } | |
| 308 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td><td class=\"c-subject\"><a href=\"") | |
| 308 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td><td class=\"c-subject\"><a href=\"") | |
| 309 | 309 | if templ_7745c5c3_Err != nil { | |
| 310 | 310 | return templ_7745c5c3_Err | |
| 311 | 311 | } | |
| 312 | 312 | var templ_7745c5c3_Var13 templ.SafeURL | |
| 313 | 313 | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Href)) | |
| 314 | 314 | if templ_7745c5c3_Err != nil { | |
| 315 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 67, Col: 59} | |
| 315 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 66, Col: 59} | |
| 316 | 316 | } | |
| 317 | 317 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | |
| 318 | 318 | if templ_7745c5c3_Err != nil { | |
| 319 | 319 | return templ_7745c5c3_Err | |
| 320 | 320 | } | |
| 321 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">") | |
| 321 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\">") | |
| 322 | 322 | if templ_7745c5c3_Err != nil { | |
| 323 | 323 | return templ_7745c5c3_Err | |
| 324 | 324 | } | |
| 325 | 325 | var templ_7745c5c3_Var14 string | |
| 326 | 326 | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title) | |
| 327 | 327 | if templ_7745c5c3_Err != nil { | |
| 328 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 67, Col: 71} | |
| 328 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 66, Col: 71} | |
| 329 | 329 | } | |
| 330 | 330 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | |
| 331 | 331 | if templ_7745c5c3_Err != nil { | |
| 332 | 332 | return templ_7745c5c3_Err | |
| 333 | 333 | } | |
| 334 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</a> <span class=\"mute\">") | |
| 334 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</a> <span class=\"mute\">") | |
| 335 | 335 | if templ_7745c5c3_Err != nil { | |
| 336 | 336 | return templ_7745c5c3_Err | |
| 337 | 337 | } | |
| 338 | 338 | var templ_7745c5c3_Var15 string | |
| 339 | 339 | templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(m.Branches) | |
| 340 | 340 | if templ_7745c5c3_Err != nil { | |
| 341 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 67, Col: 109} | |
| 341 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 66, Col: 109} | |
| 342 | 342 | } | |
| 343 | 343 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) | |
| 344 | 344 | if templ_7745c5c3_Err != nil { | |
| 345 | 345 | return templ_7745c5c3_Err | |
| 346 | 346 | } | |
| 347 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</span></td><td class=\"c-who dim\">") | |
| 347 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</span></td><td class=\"c-who dim\">") | |
| 348 | 348 | if templ_7745c5c3_Err != nil { | |
| 349 | 349 | return templ_7745c5c3_Err | |
| 350 | 350 | } | |
| 351 | 351 | var templ_7745c5c3_Var16 string | |
| 352 | 352 | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(m.Author) | |
| 353 | 353 | if templ_7745c5c3_Err != nil { | |
| 354 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 68, Col: 38} | |
| 354 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 67, Col: 38} | |
| 355 | 355 | } | |
| 356 | 356 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | |
| 357 | 357 | if templ_7745c5c3_Err != nil { | |
| 358 | 358 | return templ_7745c5c3_Err | |
| 359 | 359 | } | |
| 360 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</td><td class=\"c-when nowrap dim\">") | |
| 360 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</td><td class=\"c-when nowrap dim\">") | |
| 361 | 361 | if templ_7745c5c3_Err != nil { | |
| 362 | 362 | return templ_7745c5c3_Err | |
| 363 | 363 | } | |
| 364 | 364 | if m.WhenAt != "" { | |
| 365 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<time datetime=\"") | |
| 365 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<time datetime=\"") | |
| 366 | 366 | if templ_7745c5c3_Err != nil { | |
| 367 | 367 | return templ_7745c5c3_Err | |
| 368 | 368 | } | |
| 369 | 369 | var templ_7745c5c3_Var17 string | |
| 370 | 370 | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt) | |
| 371 | 371 | if templ_7745c5c3_Err != nil { | |
| 372 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 71, Col: 33} | |
| 372 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 70, Col: 33} | |
| 373 | 373 | } | |
| 374 | 374 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) | |
| 375 | 375 | if templ_7745c5c3_Err != nil { | |
| 376 | 376 | return templ_7745c5c3_Err | |
| 377 | 377 | } | |
| 378 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" title=\"") | |
| 378 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" title=\"") | |
| 379 | 379 | if templ_7745c5c3_Err != nil { | |
| 380 | 380 | return templ_7745c5c3_Err | |
| 381 | 381 | } | |
| 382 | 382 | var templ_7745c5c3_Var18 string | |
| 383 | 383 | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt) | |
| 384 | 384 | if templ_7745c5c3_Err != nil { | |
| 385 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 71, Col: 52} | |
| 385 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 70, Col: 52} | |
| 386 | 386 | } | |
| 387 | 387 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) | |
| 388 | 388 | if templ_7745c5c3_Err != nil { | |
| 389 | 389 | return templ_7745c5c3_Err | |
| 390 | 390 | } | |
| 391 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\">") | |
| 391 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">") | |
| 392 | 392 | if templ_7745c5c3_Err != nil { | |
| 393 | 393 | return templ_7745c5c3_Err | |
| 394 | 394 | } | |
| 395 | 395 | var templ_7745c5c3_Var19 string | |
| 396 | 396 | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(m.When) | |
| 397 | 397 | if templ_7745c5c3_Err != nil { | |
| 398 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 71, Col: 63} | |
| 398 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 70, Col: 63} | |
| 399 | 399 | } | |
| 400 | 400 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | |
| 401 | 401 | if templ_7745c5c3_Err != nil { | |
| 402 | 402 | return templ_7745c5c3_Err | |
| 403 | 403 | } | |
| 404 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</time>") | |
| 404 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</time>") | |
| 405 | 405 | if templ_7745c5c3_Err != nil { | |
| 406 | 406 | return templ_7745c5c3_Err | |
| 407 | 407 | } | |
| 408 | 408 | } | |
| 409 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</td></tr>") | |
| 409 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</td></tr>") | |
| 410 | 410 | if templ_7745c5c3_Err != nil { | |
| 411 | 411 | return templ_7745c5c3_Err | |
| 412 | 412 | } | |
| 413 | 413 | } | |
| 414 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</tbody></table>") | |
| 414 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</tbody></table>") | |
| 415 | 415 | if templ_7745c5c3_Err != nil { | |
| 416 | 416 | return templ_7745c5c3_Err | |
| 417 | 417 | } | |
| @@ -441,66 +441,66 @@ func mrListRail(p MRListPage) templ.Component { | |||
| 441 | 441 | templ_7745c5c3_Var20 = templ.NopComponent | |
| 442 | 442 | } | |
| 443 | 443 | ctx = templ.ClearChildren(ctx) | |
| 444 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<section aria-labelledby=\"r-states\"><h2 id=\"r-states\">States</h2><dl class=\"kv\"><dt>open</dt><dd>awaits review</dd><dt>merged</dt><dd>the target took the branch</dd><dt>closed</dt><dd>refused, branch kept</dd></dl></section><section aria-labelledby=\"r-new\"><h2 id=\"r-new\">Open one</h2>") | |
| 444 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<section aria-labelledby=\"r-states\"><h2 id=\"r-states\">States</h2><dl class=\"kv\"><dt>open</dt><dd>awaits review</dd><dt>merged</dt><dd>the target took the branch</dd><dt>closed</dt><dd>refused, branch kept</dd></dl></section><section aria-labelledby=\"r-new\"><h2 id=\"r-new\">Open one</h2>") | |
| 445 | 445 | if templ_7745c5c3_Err != nil { | |
| 446 | 446 | return templ_7745c5c3_Err | |
| 447 | 447 | } | |
| 448 | 448 | if len(p.Branches) > 0 { | |
| 449 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<p class=\"dim\">compare a branch with ") | |
| 449 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<p class=\"dim\">compare a branch with ") | |
| 450 | 450 | if templ_7745c5c3_Err != nil { | |
| 451 | 451 | return templ_7745c5c3_Err | |
| 452 | 452 | } | |
| 453 | 453 | var templ_7745c5c3_Var21 string | |
| 454 | 454 | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(p.Default) | |
| 455 | 455 | if templ_7745c5c3_Err != nil { | |
| 456 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 96, Col: 51} | |
| 456 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 95, Col: 51} | |
| 457 | 457 | } | |
| 458 | 458 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) | |
| 459 | 459 | if templ_7745c5c3_Err != nil { | |
| 460 | 460 | return templ_7745c5c3_Err | |
| 461 | 461 | } | |
| 462 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ":</p><nav class=\"outline\" aria-label=\"Branches\">") | |
| 462 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, ":</p><nav class=\"outline\" aria-label=\"Branches\">") | |
| 463 | 463 | if templ_7745c5c3_Err != nil { | |
| 464 | 464 | return templ_7745c5c3_Err | |
| 465 | 465 | } | |
| 466 | 466 | for _, b := range p.Branches { | |
| 467 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<a href=\"") | |
| 467 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<a href=\"") | |
| 468 | 468 | if templ_7745c5c3_Err != nil { | |
| 469 | 469 | return templ_7745c5c3_Err | |
| 470 | 470 | } | |
| 471 | 471 | var templ_7745c5c3_Var22 templ.SafeURL | |
| 472 | 472 | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href)) | |
| 473 | 473 | if templ_7745c5c3_Err != nil { | |
| 474 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 99, Col: 36} | |
| 474 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 98, Col: 36} | |
| 475 | 475 | } | |
| 476 | 476 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | |
| 477 | 477 | if templ_7745c5c3_Err != nil { | |
| 478 | 478 | return templ_7745c5c3_Err | |
| 479 | 479 | } | |
| 480 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">") | |
| 480 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\">") | |
| 481 | 481 | if templ_7745c5c3_Err != nil { | |
| 482 | 482 | return templ_7745c5c3_Err | |
| 483 | 483 | } | |
| 484 | 484 | var templ_7745c5c3_Var23 string | |
| 485 | 485 | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(b.Name) | |
| 486 | 486 | if templ_7745c5c3_Err != nil { | |
| 487 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 99, Col: 47} | |
| 487 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 98, Col: 47} | |
| 488 | 488 | } | |
| 489 | 489 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) | |
| 490 | 490 | if templ_7745c5c3_Err != nil { | |
| 491 | 491 | return templ_7745c5c3_Err | |
| 492 | 492 | } | |
| 493 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</a>") | |
| 493 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</a>") | |
| 494 | 494 | if templ_7745c5c3_Err != nil { | |
| 495 | 495 | return templ_7745c5c3_Err | |
| 496 | 496 | } | |
| 497 | 497 | } | |
| 498 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</nav>") | |
| 498 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</nav>") | |
| 499 | 499 | if templ_7745c5c3_Err != nil { | |
| 500 | 500 | return templ_7745c5c3_Err | |
| 501 | 501 | } | |
| 502 | 502 | } else { | |
| 503 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<p class=\"dim\">there is no other branch to merge yet.</p>") | |
| 503 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<p class=\"dim\">there is no other branch to merge yet.</p>") | |
| 504 | 504 | if templ_7745c5c3_Err != nil { | |
| 505 | 505 | return templ_7745c5c3_Err | |
| 506 | 506 | } | |
| @@ -509,7 +509,7 @@ func mrListRail(p MRListPage) templ.Component { | |||
| 509 | 509 | if templ_7745c5c3_Err != nil { | |
| 510 | 510 | return templ_7745c5c3_Err | |
| 511 | 511 | } | |
| 512 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</section>") | |
| 512 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</section>") | |
| 513 | 513 | if templ_7745c5c3_Err != nil { | |
| 514 | 514 | return templ_7745c5c3_Err | |
| 515 | 515 | } | |
| @@ -553,59 +553,59 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 553 | 553 | }() | |
| 554 | 554 | } | |
| 555 | 555 | ctx = templ.InitializeContext(ctx) | |
| 556 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<header class=\"show mr-h\"><h1><span class=\"mute\">") | |
| 556 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<header class=\"show mr-h\"><h1><span class=\"mute\">") | |
| 557 | 557 | if templ_7745c5c3_Err != nil { | |
| 558 | 558 | return templ_7745c5c3_Err | |
| 559 | 559 | } | |
| 560 | 560 | var templ_7745c5c3_Var26 string | |
| 561 | 561 | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number) | |
| 562 | 562 | if templ_7745c5c3_Err != nil { | |
| 563 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 115, Col: 36} | |
| 563 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 114, Col: 36} | |
| 564 | 564 | } | |
| 565 | 565 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | |
| 566 | 566 | if templ_7745c5c3_Err != nil { | |
| 567 | 567 | return templ_7745c5c3_Err | |
| 568 | 568 | } | |
| 569 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span> ") | |
| 569 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</span> ") | |
| 570 | 570 | if templ_7745c5c3_Err != nil { | |
| 571 | 571 | return templ_7745c5c3_Err | |
| 572 | 572 | } | |
| 573 | 573 | var templ_7745c5c3_Var27 string | |
| 574 | 574 | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | |
| 575 | 575 | if templ_7745c5c3_Err != nil { | |
| 576 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 115, Col: 55} | |
| 576 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 114, Col: 55} | |
| 577 | 577 | } | |
| 578 | 578 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | |
| 579 | 579 | if templ_7745c5c3_Err != nil { | |
| 580 | 580 | return templ_7745c5c3_Err | |
| 581 | 581 | } | |
| 582 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</h1><dl class=\"kv show-kv\"><dt>state</dt><dd class=\"mr-state\" data-state=\"") | |
| 582 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</h1><dl class=\"kv show-kv\"><dt>state</dt><dd class=\"mr-state\" data-state=\"") | |
| 583 | 583 | if templ_7745c5c3_Err != nil { | |
| 584 | 584 | return templ_7745c5c3_Err | |
| 585 | 585 | } | |
| 586 | 586 | var templ_7745c5c3_Var28 string | |
| 587 | 587 | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State) | |
| 588 | 588 | if templ_7745c5c3_Err != nil { | |
| 589 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 118, Col: 45} | |
| 589 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 117, Col: 45} | |
| 590 | 590 | } | |
| 591 | 591 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) | |
| 592 | 592 | if templ_7745c5c3_Err != nil { | |
| 593 | 593 | return templ_7745c5c3_Err | |
| 594 | 594 | } | |
| 595 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">") | |
| 595 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">") | |
| 596 | 596 | if templ_7745c5c3_Err != nil { | |
| 597 | 597 | return templ_7745c5c3_Err | |
| 598 | 598 | } | |
| 599 | 599 | var templ_7745c5c3_Var29 string | |
| 600 | 600 | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(p.State) | |
| 601 | 601 | if templ_7745c5c3_Err != nil { | |
| 602 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 14} | |
| 602 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 118, Col: 14} | |
| 603 | 603 | } | |
| 604 | 604 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | |
| 605 | 605 | if templ_7745c5c3_Err != nil { | |
| 606 | 606 | return templ_7745c5c3_Err | |
| 607 | 607 | } | |
| 608 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " ") | |
| 608 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " ") | |
| 609 | 609 | if templ_7745c5c3_Err != nil { | |
| 610 | 610 | return templ_7745c5c3_Err | |
| 611 | 611 | } | |
| @@ -613,53 +613,53 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 613 | 613 | var templ_7745c5c3_Var30 string | |
| 614 | 614 | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 615 | 615 | if templ_7745c5c3_Err != nil { | |
| 616 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 121, Col: 14} | |
| 616 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 14} | |
| 617 | 617 | } | |
| 618 | 618 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | |
| 619 | 619 | if templ_7745c5c3_Err != nil { | |
| 620 | 620 | return templ_7745c5c3_Err | |
| 621 | 621 | } | |
| 622 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<a href=\"") | |
| 622 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<a href=\"") | |
| 623 | 623 | if templ_7745c5c3_Err != nil { | |
| 624 | 624 | return templ_7745c5c3_Err | |
| 625 | 625 | } | |
| 626 | 626 | var templ_7745c5c3_Var31 templ.SafeURL | |
| 627 | 627 | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.MergedHref)) | |
| 628 | 628 | if templ_7745c5c3_Err != nil { | |
| 629 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 121, Col: 53} | |
| 629 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 53} | |
| 630 | 630 | } | |
| 631 | 631 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 632 | 632 | if templ_7745c5c3_Err != nil { | |
| 633 | 633 | return templ_7745c5c3_Err | |
| 634 | 634 | } | |
| 635 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\">") | |
| 635 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">") | |
| 636 | 636 | if templ_7745c5c3_Err != nil { | |
| 637 | 637 | return templ_7745c5c3_Err | |
| 638 | 638 | } | |
| 639 | 639 | var templ_7745c5c3_Var32 string | |
| 640 | 640 | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p.MergedShort) | |
| 641 | 641 | if templ_7745c5c3_Err != nil { | |
| 642 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 121, Col: 71} | |
| 642 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 71} | |
| 643 | 643 | } | |
| 644 | 644 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | |
| 645 | 645 | if templ_7745c5c3_Err != nil { | |
| 646 | 646 | return templ_7745c5c3_Err | |
| 647 | 647 | } | |
| 648 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</a>") | |
| 648 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</a>") | |
| 649 | 649 | if templ_7745c5c3_Err != nil { | |
| 650 | 650 | return templ_7745c5c3_Err | |
| 651 | 651 | } | |
| 652 | 652 | var templ_7745c5c3_Var33 string | |
| 653 | 653 | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + p.MergeMode) | |
| 654 | 654 | if templ_7745c5c3_Err != nil { | |
| 655 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 121, Col: 99} | |
| 655 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 99} | |
| 656 | 656 | } | |
| 657 | 657 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | |
| 658 | 658 | if templ_7745c5c3_Err != nil { | |
| 659 | 659 | return templ_7745c5c3_Err | |
| 660 | 660 | } | |
| 661 | 661 | } | |
| 662 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</dd><dt>branch</dt><dd>") | |
| 662 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</dd><dt>branch</dt><dd>") | |
| 663 | 663 | if templ_7745c5c3_Err != nil { | |
| 664 | 664 | return templ_7745c5c3_Err | |
| 665 | 665 | } | |
| @@ -670,7 +670,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 670 | 670 | var templ_7745c5c3_Var34 string | |
| 671 | 671 | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(" → ") | |
| 672 | 672 | if templ_7745c5c3_Err != nil { | |
| 673 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 127, Col: 14} | |
| 673 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 126, Col: 14} | |
| 674 | 674 | } | |
| 675 | 675 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | |
| 676 | 676 | if templ_7745c5c3_Err != nil { | |
| @@ -680,20 +680,20 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 680 | 680 | if templ_7745c5c3_Err != nil { | |
| 681 | 681 | return templ_7745c5c3_Err | |
| 682 | 682 | } | |
| 683 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</dd><dt>opened</dt><dd>") | |
| 683 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</dd><dt>opened</dt><dd>") | |
| 684 | 684 | if templ_7745c5c3_Err != nil { | |
| 685 | 685 | return templ_7745c5c3_Err | |
| 686 | 686 | } | |
| 687 | 687 | var templ_7745c5c3_Var35 string | |
| 688 | 688 | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name) | |
| 689 | 689 | if templ_7745c5c3_Err != nil { | |
| 690 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 132, Col: 20} | |
| 690 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 131, Col: 20} | |
| 691 | 691 | } | |
| 692 | 692 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 693 | 693 | if templ_7745c5c3_Err != nil { | |
| 694 | 694 | return templ_7745c5c3_Err | |
| 695 | 695 | } | |
| 696 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " ") | |
| 696 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " ") | |
| 697 | 697 | if templ_7745c5c3_Err != nil { | |
| 698 | 698 | return templ_7745c5c3_Err | |
| 699 | 699 | } | |
| @@ -701,93 +701,101 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 701 | 701 | var templ_7745c5c3_Var36 string | |
| 702 | 702 | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 703 | 703 | if templ_7745c5c3_Err != nil { | |
| 704 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 134, Col: 14} | |
| 704 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 14} | |
| 705 | 705 | } | |
| 706 | 706 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | |
| 707 | 707 | if templ_7745c5c3_Err != nil { | |
| 708 | 708 | return templ_7745c5c3_Err | |
| 709 | 709 | } | |
| 710 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<time datetime=\"") | |
| 710 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<time datetime=\"") | |
| 711 | 711 | if templ_7745c5c3_Err != nil { | |
| 712 | 712 | return templ_7745c5c3_Err | |
| 713 | 713 | } | |
| 714 | 714 | var templ_7745c5c3_Var37 string | |
| 715 | 715 | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt) | |
| 716 | 716 | if templ_7745c5c3_Err != nil { | |
| 717 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 134, Col: 48} | |
| 717 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 48} | |
| 718 | 718 | } | |
| 719 | 719 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37) | |
| 720 | 720 | if templ_7745c5c3_Err != nil { | |
| 721 | 721 | return templ_7745c5c3_Err | |
| 722 | 722 | } | |
| 723 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" title=\"") | |
| 723 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" title=\"") | |
| 724 | 724 | if templ_7745c5c3_Err != nil { | |
| 725 | 725 | return templ_7745c5c3_Err | |
| 726 | 726 | } | |
| 727 | 727 | var templ_7745c5c3_Var38 string | |
| 728 | 728 | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs) | |
| 729 | 729 | if templ_7745c5c3_Err != nil { | |
| 730 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 134, Col: 71} | |
| 730 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 71} | |
| 731 | 731 | } | |
| 732 | 732 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38) | |
| 733 | 733 | if templ_7745c5c3_Err != nil { | |
| 734 | 734 | return templ_7745c5c3_Err | |
| 735 | 735 | } | |
| 736 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\">") | |
| 736 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\">") | |
| 737 | 737 | if templ_7745c5c3_Err != nil { | |
| 738 | 738 | return templ_7745c5c3_Err | |
| 739 | 739 | } | |
| 740 | 740 | var templ_7745c5c3_Var39 string | |
| 741 | 741 | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When) | |
| 742 | 742 | if templ_7745c5c3_Err != nil { | |
| 743 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 134, Col: 89} | |
| 743 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 89} | |
| 744 | 744 | } | |
| 745 | 745 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | |
| 746 | 746 | if templ_7745c5c3_Err != nil { | |
| 747 | 747 | return templ_7745c5c3_Err | |
| 748 | 748 | } | |
| 749 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</time>") | |
| 749 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</time>") | |
| 750 | 750 | if templ_7745c5c3_Err != nil { | |
| 751 | 751 | return templ_7745c5c3_Err | |
| 752 | 752 | } | |
| 753 | 753 | } | |
| 754 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</dd>") | |
| 754 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</dd>") | |
| 755 | 755 | if templ_7745c5c3_Err != nil { | |
| 756 | 756 | return templ_7745c5c3_Err | |
| 757 | 757 | } | |
| 758 | 758 | if p.UpdatedAt != "" { | |
| 759 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<dt>updated</dt><dd><time datetime=\"") | |
| 759 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<dt>updated</dt><dd><time datetime=\"") | |
| 760 | 760 | if templ_7745c5c3_Err != nil { | |
| 761 | 761 | return templ_7745c5c3_Err | |
| 762 | 762 | } | |
| 763 | 763 | var templ_7745c5c3_Var40 string | |
| 764 | 764 | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt) | |
| 765 | 765 | if templ_7745c5c3_Err != nil { | |
| 766 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 139, Col: 37} | |
| 766 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 37} | |
| 767 | 767 | } | |
| 768 | 768 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40) | |
| 769 | 769 | if templ_7745c5c3_Err != nil { | |
| 770 | 770 | return templ_7745c5c3_Err | |
| 771 | 771 | } | |
| 772 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">") | |
| 772 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\">") | |
| 773 | 773 | if templ_7745c5c3_Err != nil { | |
| 774 | 774 | return templ_7745c5c3_Err | |
| 775 | 775 | } | |
| 776 | 776 | var templ_7745c5c3_Var41 string | |
| 777 | 777 | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated) | |
| 778 | 778 | if templ_7745c5c3_Err != nil { | |
| 779 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 139, Col: 51} | |
| 779 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 51} | |
| 780 | 780 | } | |
| 781 | 781 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | |
| 782 | 782 | if templ_7745c5c3_Err != nil { | |
| 783 | 783 | return templ_7745c5c3_Err | |
| 784 | 784 | } | |
| 785 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</time></dd>") | |
| 785 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</time></dd>") | |
| 786 | 786 | if templ_7745c5c3_Err != nil { | |
| 787 | 787 | return templ_7745c5c3_Err | |
| 788 | 788 | } | |
| 789 | 789 | } | |
| 790 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</dl></header><div id=\"mr-notice\"></div>") | |
| 790 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</dl></header>") | |
| 791 | + | if templ_7745c5c3_Err != nil { | |
| 792 | + | return templ_7745c5c3_Err | |
| 793 | + | } | |
| 794 | + | templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer) | |
| 795 | + | if templ_7745c5c3_Err != nil { | |
| 796 | + | return templ_7745c5c3_Err | |
| 797 | + | } | |
| 798 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, " ") | |
| 791 | 799 | if templ_7745c5c3_Err != nil { | |
| 792 | 800 | return templ_7745c5c3_Err | |
| 793 | 801 | } | |
| @@ -817,7 +825,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 817 | 825 | var templ_7745c5c3_Var42 templ.SafeURL | |
| 818 | 826 | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.AllHref)) | |
| 819 | 827 | if templ_7745c5c3_Err != nil { | |
| 820 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 150, Col: 42} | |
| 828 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 149, Col: 42} | |
| 821 | 829 | } | |
| 822 | 830 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | |
| 823 | 831 | if templ_7745c5c3_Err != nil { | |
| @@ -840,7 +848,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 840 | 848 | var templ_7745c5c3_Var43 string | |
| 841 | 849 | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 842 | 850 | if templ_7745c5c3_Err != nil { | |
| 843 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 154, Col: 49} | |
| 851 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 153, Col: 49} | |
| 844 | 852 | } | |
| 845 | 853 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | |
| 846 | 854 | if templ_7745c5c3_Err != nil { | |
| @@ -853,7 +861,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 853 | 861 | var templ_7745c5c3_Var44 templ.SafeURL | |
| 854 | 862 | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.SinceHref)) | |
| 855 | 863 | if templ_7745c5c3_Err != nil { | |
| 856 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 156, Col: 44} | |
| 864 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 155, Col: 44} | |
| 857 | 865 | } | |
| 858 | 866 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | |
| 859 | 867 | if templ_7745c5c3_Err != nil { | |
| @@ -876,7 +884,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 876 | 884 | var templ_7745c5c3_Var45 string | |
| 877 | 885 | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.ReadShort) | |
| 878 | 886 | if templ_7745c5c3_Err != nil { | |
| 879 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 160, Col: 64} | |
| 887 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 159, Col: 64} | |
| 880 | 888 | } | |
| 881 | 889 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | |
| 882 | 890 | if templ_7745c5c3_Err != nil { | |
| @@ -889,7 +897,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 889 | 897 | var templ_7745c5c3_Var46 string | |
| 890 | 898 | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.HeadShort) | |
| 891 | 899 | if templ_7745c5c3_Err != nil { | |
| 892 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 160, Col: 90} | |
| 900 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 159, Col: 90} | |
| 893 | 901 | } | |
| 894 | 902 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | |
| 895 | 903 | if templ_7745c5c3_Err != nil { | |
| @@ -912,7 +920,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 912 | 920 | var templ_7745c5c3_Var47 string | |
| 913 | 921 | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.SinceNote) | |
| 914 | 922 | if templ_7745c5c3_Err != nil { | |
| 915 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 164, Col: 34} | |
| 923 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 163, Col: 34} | |
| 916 | 924 | } | |
| 917 | 925 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) | |
| 918 | 926 | if templ_7745c5c3_Err != nil { | |
| @@ -930,7 +938,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 930 | 938 | var templ_7745c5c3_Var48 string | |
| 931 | 939 | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 932 | 940 | if templ_7745c5c3_Err != nil { | |
| 933 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 167, Col: 56} | |
| 941 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 166, Col: 56} | |
| 934 | 942 | } | |
| 935 | 943 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | |
| 936 | 944 | if templ_7745c5c3_Err != nil { | |
| @@ -943,7 +951,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 943 | 951 | var templ_7745c5c3_Var49 string | |
| 944 | 952 | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 945 | 953 | if templ_7745c5c3_Err != nil { | |
| 946 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 167, Col: 96} | |
| 954 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 166, Col: 96} | |
| 947 | 955 | } | |
| 948 | 956 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) | |
| 949 | 957 | if templ_7745c5c3_Err != nil { | |
| @@ -956,7 +964,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 956 | 964 | var templ_7745c5c3_Var50 string | |
| 957 | 965 | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 958 | 966 | if templ_7745c5c3_Err != nil { | |
| 959 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 167, Col: 141} | |
| 967 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 166, Col: 141} | |
| 960 | 968 | } | |
| 961 | 969 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) | |
| 962 | 970 | if templ_7745c5c3_Err != nil { | |
| @@ -1000,7 +1008,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 1000 | 1008 | var templ_7745c5c3_Var51 string | |
| 1001 | 1009 | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 1002 | 1010 | if templ_7745c5c3_Err != nil { | |
| 1003 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 180, Col: 29} | |
| 1011 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 179, Col: 29} | |
| 1004 | 1012 | } | |
| 1005 | 1013 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | |
| 1006 | 1014 | if templ_7745c5c3_Err != nil { | |
| @@ -1023,7 +1031,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 1023 | 1031 | var templ_7745c5c3_Var52 string | |
| 1024 | 1032 | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 1025 | 1033 | if templ_7745c5c3_Err != nil { | |
| 1026 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 185, Col: 22} | |
| 1034 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 184, Col: 22} | |
| 1027 | 1035 | } | |
| 1028 | 1036 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52) | |
| 1029 | 1037 | if templ_7745c5c3_Err != nil { | |
| @@ -1041,7 +1049,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 1041 | 1049 | var templ_7745c5c3_Var53 string | |
| 1042 | 1050 | templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 1043 | 1051 | if templ_7745c5c3_Err != nil { | |
| 1044 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 187, Col: 22} | |
| 1052 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 186, Col: 22} | |
| 1045 | 1053 | } | |
| 1046 | 1054 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53) | |
| 1047 | 1055 | if templ_7745c5c3_Err != nil { | |
| @@ -1081,7 +1089,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 1081 | 1089 | var templ_7745c5c3_Var54 string | |
| 1082 | 1090 | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved)) | |
| 1083 | 1091 | if templ_7745c5c3_Err != nil { | |
| 1084 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 200, Col: 97} | |
| 1092 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 199, Col: 97} | |
| 1085 | 1093 | } | |
| 1086 | 1094 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | |
| 1087 | 1095 | if templ_7745c5c3_Err != nil { | |
| @@ -1094,7 +1102,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 1094 | 1102 | var templ_7745c5c3_Var55 string | |
| 1095 | 1103 | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments)) | |
| 1096 | 1104 | if templ_7745c5c3_Err != nil { | |
| 1097 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 200, Col: 126} | |
| 1105 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 199, Col: 126} | |
| 1098 | 1106 | } | |
| 1099 | 1107 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 1100 | 1108 | if templ_7745c5c3_Err != nil { | |
| @@ -1175,7 +1183,7 @@ func branchLink(name, href string) templ.Component { | |||
| 1175 | 1183 | var templ_7745c5c3_Var57 templ.SafeURL | |
| 1176 | 1184 | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | |
| 1177 | 1185 | if templ_7745c5c3_Err != nil { | |
| 1178 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 218, Col: 31} | |
| 1186 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 217, Col: 31} | |
| 1179 | 1187 | } | |
| 1180 | 1188 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) | |
| 1181 | 1189 | if templ_7745c5c3_Err != nil { | |
| @@ -1188,7 +1196,7 @@ func branchLink(name, href string) templ.Component { | |||
| 1188 | 1196 | var templ_7745c5c3_Var58 string | |
| 1189 | 1197 | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(name) | |
| 1190 | 1198 | if templ_7745c5c3_Err != nil { | |
| 1191 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 218, Col: 40} | |
| 1199 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 217, Col: 40} | |
| 1192 | 1200 | } | |
| 1193 | 1201 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) | |
| 1194 | 1202 | if templ_7745c5c3_Err != nil { | |
| @@ -1202,7 +1210,7 @@ func branchLink(name, href string) templ.Component { | |||
| 1202 | 1210 | var templ_7745c5c3_Var59 string | |
| 1203 | 1211 | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(name) | |
| 1204 | 1212 | if templ_7745c5c3_Err != nil { | |
| 1205 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 220, Col: 8} | |
| 1213 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 219, Col: 8} | |
| 1206 | 1214 | } | |
| 1207 | 1215 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) | |
| 1208 | 1216 | if templ_7745c5c3_Err != nil { | |
| @@ -1284,7 +1292,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1284 | 1292 | var templ_7745c5c3_Var62 string | |
| 1285 | 1293 | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix) | |
| 1286 | 1294 | if templ_7745c5c3_Err != nil { | |
| 1287 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 42} | |
| 1295 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 42} | |
| 1288 | 1296 | } | |
| 1289 | 1297 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62) | |
| 1290 | 1298 | if templ_7745c5c3_Err != nil { | |
| @@ -1297,7 +1305,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1297 | 1305 | var templ_7745c5c3_Var63 string | |
| 1298 | 1306 | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved)) | |
| 1299 | 1307 | if templ_7745c5c3_Err != nil { | |
| 1300 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 81} | |
| 1308 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 81} | |
| 1301 | 1309 | } | |
| 1302 | 1310 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63) | |
| 1303 | 1311 | if templ_7745c5c3_Err != nil { | |
| @@ -1310,7 +1318,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1310 | 1318 | var templ_7745c5c3_Var64 string | |
| 1311 | 1319 | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1312 | 1320 | if templ_7745c5c3_Err != nil { | |
| 1313 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 242, Col: 33} | |
| 1321 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 241, Col: 33} | |
| 1314 | 1322 | } | |
| 1315 | 1323 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64)) | |
| 1316 | 1324 | if templ_7745c5c3_Err != nil { | |
| @@ -1324,7 +1332,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1324 | 1332 | var templ_7745c5c3_Var65 string | |
| 1325 | 1333 | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1326 | 1334 | if templ_7745c5c3_Err != nil { | |
| 1327 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 9} | |
| 1335 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 9} | |
| 1328 | 1336 | } | |
| 1329 | 1337 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | |
| 1330 | 1338 | if templ_7745c5c3_Err != nil { | |
| @@ -1337,7 +1345,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1337 | 1345 | var templ_7745c5c3_Var66 string | |
| 1338 | 1346 | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1339 | 1347 | if templ_7745c5c3_Err != nil { | |
| 1340 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 49} | |
| 1348 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 49} | |
| 1341 | 1349 | } | |
| 1342 | 1350 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66) | |
| 1343 | 1351 | if templ_7745c5c3_Err != nil { | |
| @@ -1350,7 +1358,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1350 | 1358 | var templ_7745c5c3_Var67 string | |
| 1351 | 1359 | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs) | |
| 1352 | 1360 | if templ_7745c5c3_Err != nil { | |
| 1353 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 65} | |
| 1361 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 65} | |
| 1354 | 1362 | } | |
| 1355 | 1363 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67) | |
| 1356 | 1364 | if templ_7745c5c3_Err != nil { | |
| @@ -1363,7 +1371,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1363 | 1371 | var templ_7745c5c3_Var68 string | |
| 1364 | 1372 | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1365 | 1373 | if templ_7745c5c3_Err != nil { | |
| 1366 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 76} | |
| 1374 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 76} | |
| 1367 | 1375 | } | |
| 1368 | 1376 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | |
| 1369 | 1377 | if templ_7745c5c3_Err != nil { | |
| @@ -1378,7 +1386,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1378 | 1386 | var templ_7745c5c3_Var69 string | |
| 1379 | 1387 | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1380 | 1388 | if templ_7745c5c3_Err != nil { | |
| 1381 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 9} | |
| 1389 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 246, Col: 9} | |
| 1382 | 1390 | } | |
| 1383 | 1391 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | |
| 1384 | 1392 | if templ_7745c5c3_Err != nil { | |
| @@ -1393,7 +1401,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1393 | 1401 | var templ_7745c5c3_Var70 string | |
| 1394 | 1402 | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1395 | 1403 | if templ_7745c5c3_Err != nil { | |
| 1396 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 250, Col: 9} | |
| 1404 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 9} | |
| 1397 | 1405 | } | |
| 1398 | 1406 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | |
| 1399 | 1407 | if templ_7745c5c3_Err != nil { | |
| @@ -1408,7 +1416,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1408 | 1416 | var templ_7745c5c3_Var71 string | |
| 1409 | 1417 | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1410 | 1418 | if templ_7745c5c3_Err != nil { | |
| 1411 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 253, Col: 9} | |
| 1419 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 9} | |
| 1412 | 1420 | } | |
| 1413 | 1421 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | |
| 1414 | 1422 | if templ_7745c5c3_Err != nil { | |
| @@ -1421,7 +1429,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1421 | 1429 | var templ_7745c5c3_Var72 string | |
| 1422 | 1430 | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where) | |
| 1423 | 1431 | if templ_7745c5c3_Err != nil { | |
| 1424 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 253, Col: 47} | |
| 1432 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 47} | |
| 1425 | 1433 | } | |
| 1426 | 1434 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) | |
| 1427 | 1435 | if templ_7745c5c3_Err != nil { | |
| @@ -1452,7 +1460,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1452 | 1460 | var templ_7745c5c3_Var73 templ.SafeURL | |
| 1453 | 1461 | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action)) | |
| 1454 | 1462 | if templ_7745c5c3_Err != nil { | |
| 1455 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 77} | |
| 1463 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 257, Col: 77} | |
| 1456 | 1464 | } | |
| 1457 | 1465 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) | |
| 1458 | 1466 | if templ_7745c5c3_Err != nil { | |
| @@ -1465,7 +1473,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1465 | 1473 | var templ_7745c5c3_Var74 string | |
| 1466 | 1474 | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value)) | |
| 1467 | 1475 | if templ_7745c5c3_Err != nil { | |
| 1468 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 259, Col: 74} | |
| 1476 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 74} | |
| 1469 | 1477 | } | |
| 1470 | 1478 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74) | |
| 1471 | 1479 | if templ_7745c5c3_Err != nil { | |
| @@ -1478,7 +1486,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1478 | 1486 | var templ_7745c5c3_Var75 string | |
| 1479 | 1487 | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label) | |
| 1480 | 1488 | if templ_7745c5c3_Err != nil { | |
| 1481 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 260, Col: 55} | |
| 1489 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 259, Col: 55} | |
| 1482 | 1490 | } | |
| 1483 | 1491 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) | |
| 1484 | 1492 | if templ_7745c5c3_Err != nil { | |
| @@ -1533,7 +1541,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1533 | 1541 | var templ_7745c5c3_Var77 templ.SafeURL | |
| 1534 | 1542 | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action)) | |
| 1535 | 1543 | if templ_7745c5c3_Err != nil { | |
| 1536 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 275, Col: 67} | |
| 1544 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 274, Col: 67} | |
| 1537 | 1545 | } | |
| 1538 | 1546 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77)) | |
| 1539 | 1547 | if templ_7745c5c3_Err != nil { | |
| @@ -1546,7 +1554,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1546 | 1554 | var templ_7745c5c3_Var78 string | |
| 1547 | 1555 | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | |
| 1548 | 1556 | if templ_7745c5c3_Err != nil { | |
| 1549 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 276, Col: 28} | |
| 1557 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 275, Col: 28} | |
| 1550 | 1558 | } | |
| 1551 | 1559 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var78) | |
| 1552 | 1560 | if templ_7745c5c3_Err != nil { | |
| @@ -1559,7 +1567,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1559 | 1567 | var templ_7745c5c3_Var79 string | |
| 1560 | 1568 | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title) | |
| 1561 | 1569 | if templ_7745c5c3_Err != nil { | |
| 1562 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 276, Col: 40} | |
| 1570 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 275, Col: 40} | |
| 1563 | 1571 | } | |
| 1564 | 1572 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79)) | |
| 1565 | 1573 | if templ_7745c5c3_Err != nil { | |
| @@ -1572,7 +1580,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1572 | 1580 | var templ_7745c5c3_Var80 string | |
| 1573 | 1581 | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | |
| 1574 | 1582 | if templ_7745c5c3_Err != nil { | |
| 1575 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 278, Col: 21} | |
| 1583 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 277, Col: 21} | |
| 1576 | 1584 | } | |
| 1577 | 1585 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80) | |
| 1578 | 1586 | if templ_7745c5c3_Err != nil { | |
| @@ -1600,7 +1608,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1600 | 1608 | var templ_7745c5c3_Var81 string | |
| 1601 | 1609 | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path) | |
| 1602 | 1610 | if templ_7745c5c3_Err != nil { | |
| 1603 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 289, Col: 50} | |
| 1611 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 288, Col: 50} | |
| 1604 | 1612 | } | |
| 1605 | 1613 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var81) | |
| 1606 | 1614 | if templ_7745c5c3_Err != nil { | |
| @@ -1613,7 +1621,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1613 | 1621 | var templ_7745c5c3_Var82 string | |
| 1614 | 1622 | templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line) | |
| 1615 | 1623 | if templ_7745c5c3_Err != nil { | |
| 1616 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 290, Col: 50} | |
| 1624 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 289, Col: 50} | |
| 1617 | 1625 | } | |
| 1618 | 1626 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82) | |
| 1619 | 1627 | if templ_7745c5c3_Err != nil { | |
| @@ -1626,7 +1634,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1626 | 1634 | var templ_7745c5c3_Var83 string | |
| 1627 | 1635 | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side) | |
| 1628 | 1636 | if templ_7745c5c3_Err != nil { | |
| 1629 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 291, Col: 50} | |
| 1637 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 290, Col: 50} | |
| 1630 | 1638 | } | |
| 1631 | 1639 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83) | |
| 1632 | 1640 | if templ_7745c5c3_Err != nil { | |
| @@ -1649,7 +1657,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1649 | 1657 | var templ_7745c5c3_Var84 templ.SafeURL | |
| 1650 | 1658 | templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel)) | |
| 1651 | 1659 | if templ_7745c5c3_Err != nil { | |
| 1652 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 296, Col: 49} | |
| 1660 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 295, Col: 49} | |
| 1653 | 1661 | } | |
| 1654 | 1662 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var84)) | |
| 1655 | 1663 | if templ_7745c5c3_Err != nil { | |
| @@ -1702,7 +1710,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1702 | 1710 | var templ_7745c5c3_Var86 string | |
| 1703 | 1711 | templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", ")) | |
| 1704 | 1712 | if templ_7745c5c3_Err != nil { | |
| 1705 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 315, Col: 36} | |
| 1713 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 314, Col: 36} | |
| 1706 | 1714 | } | |
| 1707 | 1715 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86)) | |
| 1708 | 1716 | if templ_7745c5c3_Err != nil { | |
| @@ -1730,7 +1738,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1730 | 1738 | var templ_7745c5c3_Var87 string | |
| 1731 | 1739 | templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits))) | |
| 1732 | 1740 | if templ_7745c5c3_Err != nil { | |
| 1733 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 71} | |
| 1741 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 326, Col: 71} | |
| 1734 | 1742 | } | |
| 1735 | 1743 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87)) | |
| 1736 | 1744 | if templ_7745c5c3_Err != nil { | |
| @@ -1758,7 +1766,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1758 | 1766 | var templ_7745c5c3_Var88 templ.SafeURL | |
| 1759 | 1767 | templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | |
| 1760 | 1768 | if templ_7745c5c3_Err != nil { | |
| 1761 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 36} | |
| 1769 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 330, Col: 36} | |
| 1762 | 1770 | } | |
| 1763 | 1771 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88)) | |
| 1764 | 1772 | if templ_7745c5c3_Err != nil { | |
| @@ -1771,7 +1779,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1771 | 1779 | var templ_7745c5c3_Var89 string | |
| 1772 | 1780 | templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 1773 | 1781 | if templ_7745c5c3_Err != nil { | |
| 1774 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 48} | |
| 1782 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 330, Col: 48} | |
| 1775 | 1783 | } | |
| 1776 | 1784 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89)) | |
| 1777 | 1785 | if templ_7745c5c3_Err != nil { | |
| @@ -1784,7 +1792,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1784 | 1792 | var templ_7745c5c3_Var90 string | |
| 1785 | 1793 | templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 1786 | 1794 | if templ_7745c5c3_Err != nil { | |
| 1787 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 332, Col: 32} | |
| 1795 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 32} | |
| 1788 | 1796 | } | |
| 1789 | 1797 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var90)) | |
| 1790 | 1798 | if templ_7745c5c3_Err != nil { | |
| @@ -1797,7 +1805,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1797 | 1805 | var templ_7745c5c3_Var91 string | |
| 1798 | 1806 | templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1799 | 1807 | if templ_7745c5c3_Err != nil { | |
| 1800 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 334, Col: 16} | |
| 1808 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 333, Col: 16} | |
| 1801 | 1809 | } | |
| 1802 | 1810 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var91)) | |
| 1803 | 1811 | if templ_7745c5c3_Err != nil { | |
| @@ -1811,7 +1819,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1811 | 1819 | var templ_7745c5c3_Var92 string | |
| 1812 | 1820 | templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 1813 | 1821 | if templ_7745c5c3_Err != nil { | |
| 1814 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 336, Col: 15} | |
| 1822 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 335, Col: 15} | |
| 1815 | 1823 | } | |
| 1816 | 1824 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var92)) | |
| 1817 | 1825 | if templ_7745c5c3_Err != nil { | |
| @@ -1824,7 +1832,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1824 | 1832 | var templ_7745c5c3_Var93 string | |
| 1825 | 1833 | templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1826 | 1834 | if templ_7745c5c3_Err != nil { | |
| 1827 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 336, Col: 42} | |
| 1835 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 335, Col: 42} | |
| 1828 | 1836 | } | |
| 1829 | 1837 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var93) | |
| 1830 | 1838 | if templ_7745c5c3_Err != nil { | |
| @@ -1837,7 +1845,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1837 | 1845 | var templ_7745c5c3_Var94 string | |
| 1838 | 1846 | templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1839 | 1847 | if templ_7745c5c3_Err != nil { | |
| 1840 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 336, Col: 53} | |
| 1848 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 335, Col: 53} | |
| 1841 | 1849 | } | |
| 1842 | 1850 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var94)) | |
| 1843 | 1851 | if templ_7745c5c3_Err != nil { | |
| @@ -1852,7 +1860,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1852 | 1860 | var templ_7745c5c3_Var95 string | |
| 1853 | 1861 | templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(" · new") | |
| 1854 | 1862 | if templ_7745c5c3_Err != nil { | |
| 1855 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 339, Col: 18} | |
| 1863 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 338, Col: 18} | |
| 1856 | 1864 | } | |
| 1857 | 1865 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95)) | |
| 1858 | 1866 | if templ_7745c5c3_Err != nil { | |
| @@ -1882,7 +1890,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1882 | 1890 | var templ_7745c5c3_Var96 string | |
| 1883 | 1891 | templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote) | |
| 1884 | 1892 | if templ_7745c5c3_Err != nil { | |
| 1885 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 349, Col: 34} | |
| 1893 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 348, Col: 34} | |
| 1886 | 1894 | } | |
| 1887 | 1895 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96)) | |
| 1888 | 1896 | if templ_7745c5c3_Err != nil { | |
| @@ -1900,7 +1908,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1900 | 1908 | var templ_7745c5c3_Var97 string | |
| 1901 | 1909 | templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 1902 | 1910 | if templ_7745c5c3_Err != nil { | |
| 1903 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 353, Col: 65} | |
| 1911 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 352, Col: 65} | |
| 1904 | 1912 | } | |
| 1905 | 1913 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97)) | |
| 1906 | 1914 | if templ_7745c5c3_Err != nil { | |
| @@ -1959,7 +1967,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 1959 | 1967 | var templ_7745c5c3_Var99 string | |
| 1960 | 1968 | templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort) | |
| 1961 | 1969 | if templ_7745c5c3_Err != nil { | |
| 1962 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 370, Col: 18} | |
| 1970 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 369, Col: 18} | |
| 1963 | 1971 | } | |
| 1964 | 1972 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99)) | |
| 1965 | 1973 | if templ_7745c5c3_Err != nil { | |
| @@ -1973,7 +1981,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 1973 | 1981 | var templ_7745c5c3_Var100 string | |
| 1974 | 1982 | templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 1975 | 1983 | if templ_7745c5c3_Err != nil { | |
| 1976 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 374, Col: 17} | |
| 1984 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 17} | |
| 1977 | 1985 | } | |
| 1978 | 1986 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100)) | |
| 1979 | 1987 | if templ_7745c5c3_Err != nil { | |
| @@ -1991,7 +1999,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 1991 | 1999 | var templ_7745c5c3_Var101 templ.SafeURL | |
| 1992 | 2000 | templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction)) | |
| 1993 | 2001 | if templ_7745c5c3_Err != nil { | |
| 1994 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 377, Col: 79} | |
| 2002 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 376, Col: 79} | |
| 1995 | 2003 | } | |
| 1996 | 2004 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101)) | |
| 1997 | 2005 | if templ_7745c5c3_Err != nil { | |
| @@ -2004,7 +2012,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2004 | 2012 | var templ_7745c5c3_Var102 string | |
| 2005 | 2013 | templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | |
| 2006 | 2014 | if templ_7745c5c3_Err != nil { | |
| 2007 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 378, Col: 58} | |
| 2015 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 377, Col: 58} | |
| 2008 | 2016 | } | |
| 2009 | 2017 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var102) | |
| 2010 | 2018 | if templ_7745c5c3_Err != nil { | |
| @@ -2022,7 +2030,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2022 | 2030 | var templ_7745c5c3_Var103 string | |
| 2023 | 2031 | templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value) | |
| 2024 | 2032 | if templ_7745c5c3_Err != nil { | |
| 2025 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 29} | |
| 2033 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 381, Col: 29} | |
| 2026 | 2034 | } | |
| 2027 | 2035 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var103) | |
| 2028 | 2036 | if templ_7745c5c3_Err != nil { | |
| @@ -2035,7 +2043,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2035 | 2043 | var templ_7745c5c3_Var104 string | |
| 2036 | 2044 | templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label) | |
| 2037 | 2045 | if templ_7745c5c3_Err != nil { | |
| 2038 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 41} | |
| 2046 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 381, Col: 41} | |
| 2039 | 2047 | } | |
| 2040 | 2048 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104)) | |
| 2041 | 2049 | if templ_7745c5c3_Err != nil { | |
| @@ -2058,7 +2066,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2058 | 2066 | var templ_7745c5c3_Var105 string | |
| 2059 | 2067 | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2060 | 2068 | if templ_7745c5c3_Err != nil { | |
| 2061 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 386, Col: 101} | |
| 2069 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 385, Col: 101} | |
| 2062 | 2070 | } | |
| 2063 | 2071 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) | |
| 2064 | 2072 | if templ_7745c5c3_Err != nil { | |
| @@ -2086,7 +2094,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2086 | 2094 | var templ_7745c5c3_Var106 templ.SafeURL | |
| 2087 | 2095 | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction)) | |
| 2088 | 2096 | if templ_7745c5c3_Err != nil { | |
| 2089 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 395, Col: 64} | |
| 2097 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 64} | |
| 2090 | 2098 | } | |
| 2091 | 2099 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106)) | |
| 2092 | 2100 | if templ_7745c5c3_Err != nil { | |
| @@ -2099,7 +2107,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2099 | 2107 | var templ_7745c5c3_Var107 string | |
| 2100 | 2108 | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | |
| 2101 | 2109 | if templ_7745c5c3_Err != nil { | |
| 2102 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 59} | |
| 2110 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 395, Col: 59} | |
| 2103 | 2111 | } | |
| 2104 | 2112 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var107) | |
| 2105 | 2113 | if templ_7745c5c3_Err != nil { | |
| @@ -2117,7 +2125,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2117 | 2125 | var templ_7745c5c3_Var108 templ.SafeURL | |
| 2118 | 2126 | templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction)) | |
| 2119 | 2127 | if templ_7745c5c3_Err != nil { | |
| 2120 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 64} | |
| 2128 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 399, Col: 64} | |
| 2121 | 2129 | } | |
| 2122 | 2130 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108)) | |
| 2123 | 2131 | if templ_7745c5c3_Err != nil { | |
| @@ -2187,7 +2195,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2187 | 2195 | var templ_7745c5c3_Var111 string | |
| 2188 | 2196 | templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2189 | 2197 | if templ_7745c5c3_Err != nil { | |
| 2190 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 17} | |
| 2198 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 415, Col: 17} | |
| 2191 | 2199 | } | |
| 2192 | 2200 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) | |
| 2193 | 2201 | if templ_7745c5c3_Err != nil { | |
| @@ -2200,7 +2208,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2200 | 2208 | var templ_7745c5c3_Var112 string | |
| 2201 | 2209 | templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2202 | 2210 | if templ_7745c5c3_Err != nil { | |
| 2203 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 34} | |
| 2211 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 415, Col: 34} | |
| 2204 | 2212 | } | |
| 2205 | 2213 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) | |
| 2206 | 2214 | if templ_7745c5c3_Err != nil { | |
| @@ -2213,7 +2221,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2213 | 2221 | var templ_7745c5c3_Var113 templ.SafeURL | |
| 2214 | 2222 | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) | |
| 2215 | 2223 | if templ_7745c5c3_Err != nil { | |
| 2216 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 419, Col: 45} | |
| 2224 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 45} | |
| 2217 | 2225 | } | |
| 2218 | 2226 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) | |
| 2219 | 2227 | if templ_7745c5c3_Err != nil { | |
| @@ -2226,7 +2234,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2226 | 2234 | var templ_7745c5c3_Var114 string | |
| 2227 | 2235 | templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2228 | 2236 | if templ_7745c5c3_Err != nil { | |
| 2229 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 419, Col: 58} | |
| 2237 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 58} | |
| 2230 | 2238 | } | |
| 2231 | 2239 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114)) | |
| 2232 | 2240 | if templ_7745c5c3_Err != nil { | |
| @@ -2239,7 +2247,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2239 | 2247 | var templ_7745c5c3_Var115 templ.SafeURL | |
| 2240 | 2248 | templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref)) | |
| 2241 | 2249 | if templ_7745c5c3_Err != nil { | |
| 2242 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 45} | |
| 2250 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 420, Col: 45} | |
| 2243 | 2251 | } | |
| 2244 | 2252 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115)) | |
| 2245 | 2253 | if templ_7745c5c3_Err != nil { | |
| @@ -2252,7 +2260,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2252 | 2260 | var templ_7745c5c3_Var116 string | |
| 2253 | 2261 | templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2254 | 2262 | if templ_7745c5c3_Err != nil { | |
| 2255 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 58} | |
| 2263 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 420, Col: 58} | |
| 2256 | 2264 | } | |
| 2257 | 2265 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116)) | |
| 2258 | 2266 | if templ_7745c5c3_Err != nil { | |
| @@ -2265,7 +2273,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2265 | 2273 | var templ_7745c5c3_Var117 string | |
| 2266 | 2274 | templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText) | |
| 2267 | 2275 | if templ_7745c5c3_Err != nil { | |
| 2268 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 423, Col: 21} | |
| 2276 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 422, Col: 21} | |
| 2269 | 2277 | } | |
| 2270 | 2278 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117)) | |
| 2271 | 2279 | if templ_7745c5c3_Err != nil { | |
| @@ -2283,7 +2291,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2283 | 2291 | var templ_7745c5c3_Var118 string | |
| 2284 | 2292 | templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 2285 | 2293 | if templ_7745c5c3_Err != nil { | |
| 2286 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 427, Col: 29} | |
| 2294 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 426, Col: 29} | |
| 2287 | 2295 | } | |
| 2288 | 2296 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118)) | |
| 2289 | 2297 | if templ_7745c5c3_Err != nil { | |
| @@ -2306,7 +2314,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2306 | 2314 | var templ_7745c5c3_Var119 templ.SafeURL | |
| 2307 | 2315 | templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href)) | |
| 2308 | 2316 | if templ_7745c5c3_Err != nil { | |
| 2309 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 114} | |
| 2317 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 429, Col: 114} | |
| 2310 | 2318 | } | |
| 2311 | 2319 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119)) | |
| 2312 | 2320 | if templ_7745c5c3_Err != nil { | |
| @@ -2319,7 +2327,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2319 | 2327 | var templ_7745c5c3_Var120 string | |
| 2320 | 2328 | templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label) | |
| 2321 | 2329 | if templ_7745c5c3_Err != nil { | |
| 2322 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 135} | |
| 2330 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 429, Col: 135} | |
| 2323 | 2331 | } | |
| 2324 | 2332 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120)) | |
| 2325 | 2333 | if templ_7745c5c3_Err != nil { | |
| @@ -2342,7 +2350,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2342 | 2350 | var templ_7745c5c3_Var121 templ.SafeURL | |
| 2343 | 2351 | templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action)) | |
| 2344 | 2352 | if templ_7745c5c3_Err != nil { | |
| 2345 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 433, Col: 83} | |
| 2353 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 83} | |
| 2346 | 2354 | } | |
| 2347 | 2355 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121)) | |
| 2348 | 2356 | if templ_7745c5c3_Err != nil { | |
| @@ -2355,7 +2363,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2355 | 2363 | var templ_7745c5c3_Var122 string | |
| 2356 | 2364 | templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source) | |
| 2357 | 2365 | if templ_7745c5c3_Err != nil { | |
| 2358 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 434, Col: 62} | |
| 2366 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 433, Col: 62} | |
| 2359 | 2367 | } | |
| 2360 | 2368 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122) | |
| 2361 | 2369 | if templ_7745c5c3_Err != nil { | |
| @@ -2368,7 +2376,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2368 | 2376 | var templ_7745c5c3_Var123 string | |
| 2369 | 2377 | templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target) | |
| 2370 | 2378 | if templ_7745c5c3_Err != nil { | |
| 2371 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 62} | |
| 2379 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 434, Col: 62} | |
| 2372 | 2380 | } | |
| 2373 | 2381 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123) | |
| 2374 | 2382 | if templ_7745c5c3_Err != nil { | |
| @@ -2381,7 +2389,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2381 | 2389 | var templ_7745c5c3_Var124 string | |
| 2382 | 2390 | templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title) | |
| 2383 | 2391 | if templ_7745c5c3_Err != nil { | |
| 2384 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 437, Col: 97} | |
| 2392 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 436, Col: 97} | |
| 2385 | 2393 | } | |
| 2386 | 2394 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124) | |
| 2387 | 2395 | if templ_7745c5c3_Err != nil { | |
| @@ -2404,7 +2412,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2404 | 2412 | var templ_7745c5c3_Var125 string | |
| 2405 | 2413 | templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 2406 | 2414 | if templ_7745c5c3_Err != nil { | |
| 2407 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 57} | |
| 2415 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 446, Col: 57} | |
| 2408 | 2416 | } | |
| 2409 | 2417 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125)) | |
| 2410 | 2418 | if templ_7745c5c3_Err != nil { | |
| @@ -2417,7 +2425,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2417 | 2425 | var templ_7745c5c3_Var126 string | |
| 2418 | 2426 | templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 2419 | 2427 | if templ_7745c5c3_Err != nil { | |
| 2420 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 97} | |
| 2428 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 446, Col: 97} | |
| 2421 | 2429 | } | |
| 2422 | 2430 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126)) | |
| 2423 | 2431 | if templ_7745c5c3_Err != nil { | |
| @@ -2430,7 +2438,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2430 | 2438 | var templ_7745c5c3_Var127 string | |
| 2431 | 2439 | templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 2432 | 2440 | if templ_7745c5c3_Err != nil { | |
| 2433 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 142} | |
| 2441 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 446, Col: 142} | |
| 2434 | 2442 | } | |
| 2435 | 2443 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127)) | |
| 2436 | 2444 | if templ_7745c5c3_Err != nil { | |
| @@ -2474,7 +2482,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2474 | 2482 | var templ_7745c5c3_Var128 string | |
| 2475 | 2483 | templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2476 | 2484 | if templ_7745c5c3_Err != nil { | |
| 2477 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 462, Col: 23} | |
| 2485 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 461, Col: 23} | |
| 2478 | 2486 | } | |
| 2479 | 2487 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var128) | |
| 2480 | 2488 | if templ_7745c5c3_Err != nil { | |
| @@ -2492,7 +2500,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2492 | 2500 | var templ_7745c5c3_Var129 string | |
| 2493 | 2501 | templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2494 | 2502 | if templ_7745c5c3_Err != nil { | |
| 2495 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 464, Col: 23} | |
| 2503 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 463, Col: 23} | |
| 2496 | 2504 | } | |
| 2497 | 2505 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129) | |
| 2498 | 2506 | if templ_7745c5c3_Err != nil { | |
| @@ -2562,7 +2570,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2562 | 2570 | var templ_7745c5c3_Var131 string | |
| 2563 | 2571 | templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) | |
| 2564 | 2572 | if templ_7745c5c3_Err != nil { | |
| 2565 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 480, Col: 67} | |
| 2573 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 67} | |
| 2566 | 2574 | } | |
| 2567 | 2575 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131)) | |
| 2568 | 2576 | if templ_7745c5c3_Err != nil { | |
| @@ -2580,7 +2588,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2580 | 2588 | var templ_7745c5c3_Var132 templ.SafeURL | |
| 2581 | 2589 | templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 2582 | 2590 | if templ_7745c5c3_Err != nil { | |
| 2583 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 37} | |
| 2591 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 483, Col: 37} | |
| 2584 | 2592 | } | |
| 2585 | 2593 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132)) | |
| 2586 | 2594 | if templ_7745c5c3_Err != nil { | |
| @@ -2593,7 +2601,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2593 | 2601 | var templ_7745c5c3_Var133 string | |
| 2594 | 2602 | templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 2595 | 2603 | if templ_7745c5c3_Err != nil { | |
| 2596 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 49} | |
| 2604 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 483, Col: 49} | |
| 2597 | 2605 | } | |
| 2598 | 2606 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133)) | |
| 2599 | 2607 | if templ_7745c5c3_Err != nil { | |
| @@ -2606,7 +2614,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2606 | 2614 | var templ_7745c5c3_Var134 string | |
| 2607 | 2615 | templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 2608 | 2616 | if templ_7745c5c3_Err != nil { | |
| 2609 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 485, Col: 32} | |
| 2617 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 32} | |
| 2610 | 2618 | } | |
| 2611 | 2619 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134)) | |
| 2612 | 2620 | if templ_7745c5c3_Err != nil { | |
| @@ -2619,7 +2627,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2619 | 2627 | var templ_7745c5c3_Var135 string | |
| 2620 | 2628 | templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 2621 | 2629 | if templ_7745c5c3_Err != nil { | |
| 2622 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 16} | |
| 2630 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 16} | |
| 2623 | 2631 | } | |
| 2624 | 2632 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135)) | |
| 2625 | 2633 | if templ_7745c5c3_Err != nil { | |
| @@ -2633,7 +2641,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2633 | 2641 | var templ_7745c5c3_Var136 string | |
| 2634 | 2642 | templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 2635 | 2643 | if templ_7745c5c3_Err != nil { | |
| 2636 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 15} | |
| 2644 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 488, Col: 15} | |
| 2637 | 2645 | } | |
| 2638 | 2646 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136)) | |
| 2639 | 2647 | if templ_7745c5c3_Err != nil { | |
| @@ -2646,7 +2654,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2646 | 2654 | var templ_7745c5c3_Var137 string | |
| 2647 | 2655 | templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 2648 | 2656 | if templ_7745c5c3_Err != nil { | |
| 2649 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 42} | |
| 2657 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 488, Col: 42} | |
| 2650 | 2658 | } | |
| 2651 | 2659 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var137) | |
| 2652 | 2660 | if templ_7745c5c3_Err != nil { | |
| @@ -2659,7 +2667,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2659 | 2667 | var templ_7745c5c3_Var138 string | |
| 2660 | 2668 | templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 2661 | 2669 | if templ_7745c5c3_Err != nil { | |
| 2662 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 53} | |
| 2670 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 488, Col: 53} | |
| 2663 | 2671 | } | |
| 2664 | 2672 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138)) | |
| 2665 | 2673 | if templ_7745c5c3_Err != nil { | |
| @@ -2687,7 +2695,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2687 | 2695 | var templ_7745c5c3_Var139 string | |
| 2688 | 2696 | templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2689 | 2697 | if templ_7745c5c3_Err != nil { | |
| 2690 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 47} | |
| 2698 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 495, Col: 47} | |
| 2691 | 2699 | } | |
| 2692 | 2700 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139)) | |
| 2693 | 2701 | if templ_7745c5c3_Err != nil { | |
| @@ -2710,7 +2718,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2710 | 2718 | var templ_7745c5c3_Var140 string | |
| 2711 | 2719 | templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 2712 | 2720 | if templ_7745c5c3_Err != nil { | |
| 2713 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 501, Col: 66} | |
| 2721 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 500, Col: 66} | |
| 2714 | 2722 | } | |
| 2715 | 2723 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140)) | |
| 2716 | 2724 | if templ_7745c5c3_Err != nil { | |
M internal/adapters/web/overlays.templ +4 -10
| @@ -37,8 +37,8 @@ templ OverlayRefs(l Layout) { | |||
| 37 | 37 | type="search" | |
| 38 | 38 | placeholder="Switch branch or tag…" | |
| 39 | 39 | autocomplete="off" | |
| 40 | - | data-pick | |
| 41 | - | hx-on:input="let t = this.value.toLowerCase(); q('#panel-refs li').arr().forEach(li => li.hidden = !li.textContent.toLowerCase().includes(t))" | |
| 40 | + | data-pick="#panel-refs li" | |
| 41 | + | hx-on:input="ermite.filter(this)" | |
| 42 | 42 | /> | |
| 43 | 43 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"><kbd>esc</kbd></button> | |
| 44 | 44 | </div> | |
| @@ -96,14 +96,8 @@ templ OverlayCmd(items []CmdItem) { | |||
| 96 | 96 | autocomplete="off" | |
| 97 | 97 | spellcheck="false" | |
| 98 | 98 | aria-controls="cmd-list" | |
| 99 | - | hx-on:input="let t = this.value.toLowerCase().trim(); | |
| 100 | - | q('#cmd-list > li').arr().forEach(li => li.hidden = !li.dataset.k.includes(t)); | |
| 101 | - | let first = q('#cmd-list > li:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#cmd-list > li')" | |
| 102 | - | hx-on:keydown="let vis = q('#cmd-list > li:not([hidden])').arr(); | |
| 103 | - | let i = vis.findIndex(li => li.hasAttribute('data-cursor')); | |
| 104 | - | let step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0; | |
| 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' })) } | |
| 106 | - | else if (event.key === 'Enter') { event.preventDefault(); vis[Math.max(i, 0)]?.querySelector('a').click() }" | |
| 99 | + | hx-on:input="ermite.cmdFilter(this)" | |
| 100 | + | hx-on:keydown="ermite.cmdKey(event)" | |
| 107 | 101 | /> | |
| 108 | 102 | <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"><kbd>esc</kbd></button> | |
| 109 | 103 | </div> | |
M internal/adapters/web/overlays_templ.go +6 -6 generated file: collapsed
M internal/adapters/web/pages.templ +4 -5
| @@ -10,10 +10,10 @@ templ Home(l Layout, p HomePage) { | |||
| 10 | 10 | <input | |
| 11 | 11 | id="home-filter" | |
| 12 | 12 | type="search" | |
| 13 | - | data-filter | |
| 13 | + | data-filter="#repo-rows > tr" | |
| 14 | 14 | placeholder="Filter repositories…" | |
| 15 | 15 | autocomplete="off" | |
| 16 | - | hx-on:input="let t = this.value.toLowerCase(); q('#repo-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#repo-rows > tr:not([hidden])')" | |
| 16 | + | hx-on:input="ermite.filter(this)" | |
| 17 | 17 | /> | |
| 18 | 18 | </div> | |
| 19 | 19 | <table class="list home" aria-label="Repositories"> | |
| @@ -94,11 +94,10 @@ templ Refs(l Layout, p RefsPage) { | |||
| 94 | 94 | <input | |
| 95 | 95 | id="ref-filter" | |
| 96 | 96 | type="search" | |
| 97 | - | data-filter | |
| 97 | + | data-filter="#ref-rows > tr" | |
| 98 | 98 | placeholder="Filter branches and tags…" | |
| 99 | 99 | autocomplete="off" | |
| 100 | - | hx-on:input="let t = this.value.toLowerCase(); q('#ref-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | |
| 101 | - | let first = q('#ref-rows > tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '#ref-rows > tr')" | |
| 100 | + | hx-on:input="ermite.filter(this)" | |
| 102 | 101 | /> | |
| 103 | 102 | </div> | |
| 104 | 103 | <table class="list refs" aria-label="Branches and tags"> | |
M internal/adapters/web/pages_templ.go +22 -22 generated file: collapsed
M internal/adapters/web/shell.templ +7 -8
| @@ -83,34 +83,33 @@ templ Header(l Layout) { | |||
| 83 | 83 | } | |
| 84 | 84 | <div class="hdr-acts"> | |
| 85 | 85 | if l.Ref != "" { | |
| 86 | - | <button | |
| 87 | - | type="button" | |
| 86 | + | <a | |
| 88 | 87 | class="btn ref" | |
| 89 | - | aria-haspopup="dialog" | |
| 90 | - | aria-controls="panel-refs" | |
| 88 | + | href={ templ.SafeURL(repoHref(l.Repo, "refs")) } | |
| 89 | + | data-fire="refs" | |
| 91 | 90 | aria-keyshortcuts="b" | |
| 92 | 91 | title="switch branch or tag (b)" | |
| 93 | - | hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))" | |
| 94 | 92 | > | |
| 95 | 93 | <span class="ico i-branch" aria-hidden="true"></span> | |
| 96 | 94 | <span class="ref-name">{ l.Ref }</span> | |
| 97 | 95 | <span class="sha">{ l.Short }</span> | |
| 98 | - | </button> | |
| 96 | + | </a> | |
| 99 | 97 | } | |
| 100 | 98 | <button | |
| 101 | 99 | type="button" | |
| 102 | 100 | class="btn search" | |
| 101 | + | data-js | |
| 103 | 102 | aria-haspopup="dialog" | |
| 104 | 103 | aria-controls="panel-cmd" | |
| 105 | 104 | aria-keyshortcuts="Control+K :" | |
| 106 | 105 | title="go to a section, ref, file or repo (ctrl k or :)" | |
| 107 | - | hx-on:click="document.body.dispatchEvent(new CustomEvent('k-cmd'))" | |
| 106 | + | data-fire="cmd" | |
| 108 | 107 | > | |
| 109 | 108 | <span class="ico i-search" aria-hidden="true"></span> | |
| 110 | 109 | <span class="search-label">Go to…</span> | |
| 111 | 110 | <kbd aria-hidden="true">ctrl k</kbd> | |
| 112 | 111 | </button> | |
| 113 | - | <button type="button" class="btn icon" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-help'))">?</button> | |
| 112 | + | <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button> | |
| 114 | 113 | if SignedIn(ctx) { | |
| 115 | 114 | <form class="owner" method="post" action="/-/logout"> | |
| 116 | 115 | <span class="owner-badge" title="signed in as the owner">owner</span> | |
M internal/adapters/web/shell_templ.go +74 -61 generated file: collapsed
M internal/adapters/web/static/css/10-base.css +2 -0
| @@ -70,6 +70,8 @@ | |||
| 70 | 70 | } | |
| 71 | 71 | ||
| 72 | 72 | @layer utilities { | |
| 73 | + | /* what does nothing without scripts waits for ermite.js, which sets html.js */ | |
| 74 | + | html:not(.js) [data-js], html:not(.js) .filter { display: none; } | |
| 73 | 75 | .dim { color: var(--ink-dim); } | |
| 74 | 76 | .mute { color: var(--ink-mute); } | |
| 75 | 77 | .nowrap { white-space: nowrap; } | |
M internal/adapters/web/static/css/20-shell.css +6 -1
| @@ -169,7 +169,12 @@ | |||
| 169 | 169 | & > :empty { display: none; } | |
| 170 | 170 | & .hint-live { color: var(--red); } | |
| 171 | 171 | } | |
| 172 | - | .hint:not(:has(> :not(:empty))) { display: none; } | |
| 172 | + | /* empty, the badge is out of sight but stays a status region (display: none | |
| 173 | + | would drop it from the accessibility tree and its next message would not be | |
| 174 | + | announced) */ | |
| 175 | + | .hint:not(:has(> :not(:empty))) { | |
| 176 | + | width: 1px; height: 1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); box-shadow: none; | |
| 177 | + | } | |
| 173 | 178 | ||
| 174 | 179 | @media (max-width: 1080px) { | |
| 175 | 180 | /* the rail goes under the content, in two columns while there is room */ | |
M internal/adapters/web/static/css/80-live.css +7 -4
| @@ -1,9 +1,12 @@ | |||
| 1 | 1 | @layer components { | |
| 2 | - | /* live updates: the banner of new commits, an empty slot takes no room */ | |
| 3 | - | #commits-head:empty { display: none; } | |
| 4 | - | .live-banner { | |
| 2 | + | /* live updates: a slot is a status region that stays in the page; its banner | |
| 3 | + | comes and goes as its content. Empty, it is out of the way but still in the | |
| 4 | + | accessibility tree (display: none would drop the region, and the next banner | |
| 5 | + | would not be announced). */ | |
| 6 | + | .live-slot:empty { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } | |
| 7 | + | .live-slot:not(:empty) { | |
| 5 | 8 | padding: var(--s3) var(--s4); font-weight: 700; | |
| 6 | 9 | background: var(--bg-surface); border: 1px solid var(--border-strong); border-left: 3px solid var(--teal); border-radius: var(--r-s); | |
| 7 | 10 | } | |
| 8 | - | .live-banner a { color: var(--ink-hi); } | |
| 11 | + | .live-slot a { color: var(--ink-hi); } | |
| 9 | 12 | } | |
M internal/adapters/web/static/js/ermite.js +279 -14
| @@ -1,50 +1,297 @@ | |||
| 1 | - | // ermite.js: the one thing hx-live cannot do is a global key *sequence* state machine | |
| 2 | - | // ("g" then "h"), nor ignore keystrokes aimed at inputs before any element is focused. | |
| 3 | - | // It only translates keys into DOM events named "k-<name>" on <body>; every reaction | |
| 4 | - | // (cursor, panels, filters, permalink) is declared in templates/components/keys.tmpl with hx-on + hx-live. | |
| 1 | + | // ermite.js: the behaviour of the pages that htmx and hx-live do not give. | |
| 2 | + | // | |
| 3 | + | // 1. Keys. A global key *sequence* state machine ("g" then "h") and the | |
| 4 | + | // keystrokes aimed at inputs cannot be declared with hx-on. The keydown | |
| 5 | + | // handler only translates keys into DOM events named "k-<name>" on <body>; | |
| 6 | + | // keys.templ binds each of them (hx-on, one line each) to a function of | |
| 7 | + | // window.ermite below, which does the work: row cursor, dialogs, filters, | |
| 8 | + | // diff layout, permalink. | |
| 9 | + | // 2. Clicks on [data-fire] elements fire the same events (header buttons). | |
| 10 | + | // 3. ermiteLive applies the fragments of the event stream (see events.go). | |
| 11 | + | // 4. The file of the diff in view is marked in the table of contents. | |
| 12 | + | // 5. A handler that fails is shown in the hint badge. | |
| 5 | 13 | (() => { | |
| 14 | + | // Marks the page as scripted: what does nothing without scripts (the filters, | |
| 15 | + | // the "go to" and help buttons) is hidden until this class is set (see the | |
| 16 | + | // utilities of 10-base.css). | |
| 17 | + | document.documentElement.classList.add('js'); | |
| 18 | + | ||
| 19 | + | const $ = (sel, root = document) => root.querySelector(sel); | |
| 20 | + | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | |
| 21 | + | const body = () => document.body; | |
| 22 | + | ||
| 23 | + | // flash shows a message in the hint badge (body[data-flash], a status | |
| 24 | + | // region), then clears it unless another message came in the meantime. | |
| 25 | + | let flashTimer = 0; | |
| 26 | + | const flash = (msg, ms = 2000) => { | |
| 27 | + | body().dataset.flash = msg; | |
| 28 | + | clearTimeout(flashTimer); | |
| 29 | + | flashTimer = setTimeout(() => (body().dataset.flash = ''), ms); | |
| 30 | + | }; | |
| 31 | + | ||
| 32 | + | const visibleRows = () => $$('[data-rows] > :not([hidden])'); | |
| 33 | + | // setCursor puts data-cursor on el and takes it from the others. | |
| 34 | + | const setCursor = (el, others) => { | |
| 35 | + | for (const o of others) if (o !== el) o.removeAttribute('data-cursor'); | |
| 36 | + | el.setAttribute('data-cursor', ''); | |
| 37 | + | }; | |
| 38 | + | const scrollPage = (to) => { | |
| 39 | + | scrollTo(0, to); | |
| 40 | + | $('#main')?.scrollTo(0, to); | |
| 41 | + | }; | |
| 42 | + | ||
| 43 | + | const ermite = { | |
| 44 | + | fire(name) { | |
| 45 | + | body().dispatchEvent(new CustomEvent('k-' + name, { bubbles: false })); | |
| 46 | + | }, | |
| 47 | + | ||
| 48 | + | // Row cursor: j / k. | |
| 49 | + | move(d) { | |
| 50 | + | if ($('dialog[open]')) return; | |
| 51 | + | const rows = visibleRows(); | |
| 52 | + | if (!rows.length) { | |
| 53 | + | scrollBy(0, d * 180); | |
| 54 | + | $('#main')?.scrollBy(0, d * 180); | |
| 55 | + | return; | |
| 56 | + | } | |
| 57 | + | const i = rows.findIndex((r) => r.hasAttribute('data-cursor')); | |
| 58 | + | const j = Math.max(0, Math.min(rows.length - 1, i + d)); | |
| 59 | + | setCursor(rows[j], $$('[data-rows] > [data-cursor]')); | |
| 60 | + | rows[j].scrollIntoView({ block: 'nearest' }); | |
| 61 | + | const mark = j + 1 + '/' + rows.length; | |
| 62 | + | body().dataset.nav = mark; | |
| 63 | + | setTimeout(() => body().dataset.nav === mark && (body().dataset.nav = ''), 1500); | |
| 64 | + | }, | |
| 65 | + | // gg / G: first or last row, or the top or bottom of the page. | |
| 66 | + | edge(last) { | |
| 67 | + | const rows = visibleRows(); | |
| 68 | + | const row = last ? rows.at(-1) : rows[0]; | |
| 69 | + | if (row) row.scrollIntoView({ block: 'nearest' }); | |
| 70 | + | else scrollPage(last ? 1e9 : 0); | |
| 71 | + | }, | |
| 72 | + | open() { | |
| 73 | + | if (!$('dialog[open]')) $('[data-cursor] a')?.click(); | |
| 74 | + | }, | |
| 75 | + | focusFilter() { | |
| 76 | + | $('[data-filter]')?.focus(); | |
| 77 | + | }, | |
| 78 | + | go(sel) { | |
| 79 | + | $(sel)?.click(); | |
| 80 | + | }, | |
| 81 | + | tab(key) { | |
| 82 | + | $(`.tabs [data-key=${key}]`)?.click(); | |
| 83 | + | }, | |
| 84 | + | ||
| 85 | + | // Dialogs. | |
| 86 | + | closeDialogs() { | |
| 87 | + | for (const d of $$('dialog[open]')) d.close(); | |
| 88 | + | }, | |
| 89 | + | dialog(id, focusId) { | |
| 90 | + | const d = document.getElementById(id); | |
| 91 | + | if (!d) return; | |
| 92 | + | ermite.closeDialogs(); | |
| 93 | + | d.showModal(); | |
| 94 | + | if (focusId) document.getElementById(focusId)?.focus(); | |
| 95 | + | }, | |
| 96 | + | ||
| 97 | + | // filter hides the rows that do not contain the text of an input. The | |
| 98 | + | // input says which rows (data-filter, or data-pick for a list without a | |
| 99 | + | // cursor) and, optionally, the groups to hide when they have no row left | |
| 100 | + | // (data-groups). The cursor goes to the first row left. | |
| 101 | + | filter(input) { | |
| 102 | + | const sel = input.dataset.filter || input.dataset.pick; | |
| 103 | + | const t = input.value.toLowerCase(); | |
| 104 | + | const rows = $$(sel); | |
| 105 | + | for (const r of rows) r.hidden = !r.textContent.toLowerCase().includes(t); | |
| 106 | + | if (input.dataset.groups) { | |
| 107 | + | for (const g of $$(input.dataset.groups)) g.hidden = !rows.some((r) => !r.hidden && g.contains(r)); | |
| 108 | + | } | |
| 109 | + | if (input.dataset.filter) { | |
| 110 | + | const first = rows.find((r) => !r.hidden); | |
| 111 | + | for (const r of rows) r.toggleAttribute('data-cursor', r === first); | |
| 112 | + | } | |
| 113 | + | }, | |
| 114 | + | // The "go to" palette. | |
| 115 | + | cmdFilter(input) { | |
| 116 | + | const t = input.value.toLowerCase().trim(); | |
| 117 | + | const items = $$('#cmd-list > li'); | |
| 118 | + | for (const li of items) li.hidden = !li.dataset.k.includes(t); | |
| 119 | + | const first = items.find((li) => !li.hidden); | |
| 120 | + | if (first) setCursor(first, items); | |
| 121 | + | }, | |
| 122 | + | cmdKey(e) { | |
| 123 | + | const vis = $$('#cmd-list > li:not([hidden])'); | |
| 124 | + | const i = vis.findIndex((li) => li.hasAttribute('data-cursor')); | |
| 125 | + | const step = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : 0; | |
| 126 | + | if (step) { | |
| 127 | + | e.preventDefault(); | |
| 128 | + | const n = vis[Math.max(0, Math.min(vis.length - 1, i + step))]; | |
| 129 | + | if (n) { | |
| 130 | + | setCursor(n, $$('#cmd-list > li')); | |
| 131 | + | n.scrollIntoView({ block: 'nearest' }); | |
| 132 | + | } | |
| 133 | + | } else if (e.key === 'Enter') { | |
| 134 | + | e.preventDefault(); | |
| 135 | + | vis[Math.max(i, 0)]?.querySelector('a')?.click(); | |
| 136 | + | } | |
| 137 | + | }, | |
| 138 | + | ||
| 139 | + | // Diff. | |
| 140 | + | layout(mode) { | |
| 141 | + | const el = $('[data-diff]'); | |
| 142 | + | if (!el) return; | |
| 143 | + | el.dataset.diff = mode; | |
| 144 | + | try { sessionStorage.setItem('diff', mode); } catch { /* storage refused: the layout holds for this page */ } | |
| 145 | + | for (const d of $$('[data-file]')) if ($('.dt', d)) d.open = true; | |
| 146 | + | }, | |
| 147 | + | collapse() { | |
| 148 | + | const files = $$('[data-file]'); | |
| 149 | + | const open = !files.some((d) => d.open); | |
| 150 | + | for (const d of files) d.open = open; | |
| 151 | + | }, | |
| 152 | + | nextNote() { | |
| 153 | + | const notes = $$('#diffs .cmt'); | |
| 154 | + | if (!notes.length) return; | |
| 155 | + | const i = notes.findIndex((n) => n.getBoundingClientRect().top > innerHeight / 3); | |
| 156 | + | const n = notes[i < 0 ? 0 : i]; | |
| 157 | + | const d = n.closest('details'); | |
| 158 | + | if (d) d.open = true; | |
| 159 | + | n.scrollIntoView({ block: 'center' }); | |
| 160 | + | }, | |
| 161 | + | ||
| 162 | + | // y: pin the URL to the commit and copy it. | |
| 163 | + | async permalink() { | |
| 164 | + | const b = body().dataset; | |
| 165 | + | if (!b.sha) return flash('no ref to pin on this page'); | |
| 166 | + | history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash); | |
| 167 | + | try { | |
| 168 | + | await navigator.clipboard.writeText(location.href); | |
| 169 | + | flash('permalink ' + b.sha.slice(0, 7) + ' copied'); | |
| 170 | + | } catch { | |
| 171 | + | flash('permalink in the address bar (copy refused)'); | |
| 172 | + | } | |
| 173 | + | }, | |
| 174 | + | ||
| 175 | + | // The [copy] button of a clone line: the label says "Copied" for 1.5 s, | |
| 176 | + | // and so does the accessible name; the hint badge (a status region) | |
| 177 | + | // announces it. | |
| 178 | + | async copy(btn) { | |
| 179 | + | const line = btn.closest('.clone-line')?.querySelector('code')?.textContent ?? ''; | |
| 180 | + | btn.dataset.idleName ||= btn.getAttribute('aria-label') || ''; | |
| 181 | + | let ok = true; | |
| 182 | + | try { await navigator.clipboard.writeText(line); } catch { ok = false; } | |
| 183 | + | btn.textContent = ok ? 'Copied' : 'Copy failed'; | |
| 184 | + | btn.setAttribute('aria-label', (ok ? 'copied: ' : 'copy failed: ') + line); | |
| 185 | + | flash(ok ? 'copied' : 'copy failed', 1500); | |
| 186 | + | clearTimeout(btn._restore); | |
| 187 | + | btn._restore = setTimeout(() => { | |
| 188 | + | btn.textContent = btn.dataset.label; | |
| 189 | + | btn.setAttribute('aria-label', btn.dataset.idleName); | |
| 190 | + | }, 1500); | |
| 191 | + | }, | |
| 192 | + | }; | |
| 193 | + | window.ermite = ermite; | |
| 194 | + | ||
| 195 | + | // 1. Keys. | |
| 6 | 196 | const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink', | |
| 7 | 197 | s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc' }; | |
| 8 | - | const goto = { h: 'gh', t: 'gt', c: 'gc', r: 'gr', m: 'gm', g: 'top' }; | |
| 198 | + | // "g t" is the key "t": the tabs have one event each. | |
| 199 | + | const goto = { h: 'gh', t: 't', c: 'c', r: 'r', m: 'm', g: 'top' }; | |
| 9 | 200 | let pending = 0; | |
| 10 | - | const fire = (name) => document.body.dispatchEvent(new CustomEvent('k-' + name, { bubbles: false })); | |
| 11 | 201 | addEventListener('keydown', (e) => { | |
| 12 | 202 | const typing = e.target.closest('input, textarea, select, [contenteditable]'); | |
| 13 | 203 | // ctrl k / cmd k opens the palette; in a field only cmd k, so that the | |
| 14 | 204 | // field keeps ctrl k (kill to end of line on macOS). | |
| 15 | - | if ((e.metaKey || (e.ctrlKey && !typing)) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'k') { e.preventDefault(); pending = 0; return fire('cmd'); } | |
| 205 | + | if ((e.metaKey || (e.ctrlKey && !typing)) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'k') { e.preventDefault(); pending = 0; return ermite.fire('cmd'); } | |
| 16 | 206 | if (e.ctrlKey || e.metaKey || e.altKey) return; | |
| 17 | - | if (e.key === 'Escape') { pending = 0; if (typing) e.target.blur(); return fire('esc'); } | |
| 207 | + | if (e.key === 'Escape') { pending = 0; if (typing) e.target.blur(); return ermite.fire('esc'); } | |
| 18 | 208 | if (typing) return; | |
| 19 | 209 | // An open dialog takes only the keys that move between dialogs: the page | |
| 20 | 210 | // behind it does not react (scripted clicks would go through inert). | |
| 21 | - | if (document.querySelector('dialog[open]') && !['?', 'b', ':', 'f', 'Enter'].includes(e.key)) { pending = 0; return; } | |
| 211 | + | if ($('dialog[open]') && !['?', 'b', ':', 'f', 'Enter'].includes(e.key)) { pending = 0; return; } | |
| 22 | 212 | if (e.key === 'Enter' && e.target.closest('a, button, summary')) return; | |
| 23 | 213 | if (pending) { | |
| 24 | 214 | const name = goto[e.key]; | |
| 25 | 215 | pending = 0; | |
| 26 | - | if (name) { e.preventDefault(); fire(name); } | |
| 216 | + | if (name) { e.preventDefault(); ermite.fire(name); } | |
| 27 | 217 | return; | |
| 28 | 218 | } | |
| 29 | 219 | if (e.key === 'g') { pending = 1; setTimeout(() => (pending = 0), 1200); return; } | |
| 30 | 220 | const name = single[e.key]; | |
| 31 | 221 | if (!name) return; | |
| 32 | - | if (e.key !== 'Enter' || !document.querySelector('dialog[open]')) e.preventDefault(); | |
| 33 | - | fire(name); | |
| 222 | + | if (e.key !== 'Enter' || !$('dialog[open]')) e.preventDefault(); | |
| 223 | + | ermite.fire(name); | |
| 224 | + | }); | |
| 225 | + | ||
| 226 | + | // 2. A [data-fire] element does what the key of that name does (the header | |
| 227 | + | // buttons, the diff layout links). The link to the refs page works without | |
| 228 | + | // scripts and is upgraded here to open the dialog; a click with a modifier | |
| 229 | + | // keeps its meaning (new tab). | |
| 230 | + | addEventListener('click', (e) => { | |
| 231 | + | const el = e.target.closest?.('[data-fire]'); | |
| 232 | + | if (!el || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; | |
| 233 | + | e.preventDefault(); | |
| 234 | + | ermite.fire(el.dataset.fire); | |
| 34 | 235 | }); | |
| 236 | + | for (const a of $$('a[data-fire="refs"]')) { | |
| 237 | + | a.setAttribute('aria-haspopup', 'dialog'); | |
| 238 | + | a.setAttribute('aria-controls', 'panel-refs'); | |
| 239 | + | } | |
| 240 | + | ||
| 35 | 241 | // In the app shell the page does not scroll, <main> does: give it focus so arrows, | |
| 36 | 242 | // PageUp/PageDown and space scroll the content without a click first. | |
| 37 | 243 | if (!location.hash) document.getElementById('main')?.focus({ preventScroll: true }); | |
| 244 | + | ||
| 245 | + | // 4. The table of contents of a diff marks the file in view: the last file | |
| 246 | + | // whose top is above the first third of the window (the first file when none | |
| 247 | + | // is). One observer watches the files against that band; a file has passed | |
| 248 | + | // the line when it is in the band or wholly above it. It reports only when a | |
| 249 | + | // file enters or leaves the band, so a scroll costs nothing in between. | |
| 250 | + | const files = $$('.dfile'); | |
| 251 | + | const links = new Map($$('.files a[href^="#"]').map((a) => [a.hash.slice(1), a])); | |
| 252 | + | if (files.length && links.size && 'IntersectionObserver' in window) { | |
| 253 | + | const passed = new Set(); | |
| 254 | + | let current = null; | |
| 255 | + | const mark = () => { | |
| 256 | + | const f = files.findLast((x) => passed.has(x)) ?? files[0]; | |
| 257 | + | if (f === current) return; | |
| 258 | + | links.get(current?.id)?.removeAttribute('aria-current'); | |
| 259 | + | links.get(f.id)?.setAttribute('aria-current', 'true'); | |
| 260 | + | current = f; | |
| 261 | + | }; | |
| 262 | + | const io = new IntersectionObserver((entries) => { | |
| 263 | + | for (const e of entries) { | |
| 264 | + | const line = e.rootBounds ? e.rootBounds.bottom : innerHeight / 3; | |
| 265 | + | if (e.isIntersecting || e.boundingClientRect.top < line) passed.add(e.target); | |
| 266 | + | else passed.delete(e.target); | |
| 267 | + | } | |
| 268 | + | mark(); | |
| 269 | + | }, { rootMargin: '0px 0px -67% 0px' }); | |
| 270 | + | for (const f of files) io.observe(f); | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | // 5. A failure of a handler or of a request (htmx logs it to the console | |
| 274 | + | // already) is shown, so that a key that does nothing is not a mystery. | |
| 275 | + | addEventListener('htmx:error', (e) => { | |
| 276 | + | if (e.target.closest?.('.hint-live')) return; // the stream says so itself ("live updates paused") | |
| 277 | + | const err = e.detail?.error; | |
| 278 | + | flash('something went wrong' + (err?.message ? ': ' + err.message : ''), 4000); | |
| 279 | + | }); | |
| 38 | 280 | })(); | |
| 39 | 281 | ||
| 40 | 282 | // ermiteLive applies the fragments of the event stream (/-/events, see | |
| 41 | 283 | // internal/adapters/web/events.go). A fragment is a top-level element with an | |
| 42 | 284 | // id: it replaces the element of the page that has the same id. A fragment | |
| 43 | 285 | // marked data-gone removes it; a row of a repository the home page does not | |
| 44 | - | // list yet is added at the top of the list. A gap, which says the page may | |
| 45 | - | // have missed events or that what it shows changed, reloads the page. | |
| 286 | + | // list yet is added at the top of the list. A slot (data-slot: the place of a | |
| 287 | + | // banner, a persistent status region) keeps its element and only gets the | |
| 288 | + | // children of the fragment, so that screen readers announce the change. A gap, | |
| 289 | + | // which says the page may have missed events or that what it shows changed, | |
| 290 | + | // reloads the page. A stream the server refuses on reconnection (the | |
| 291 | + | // repository is gone, the session ended) makes the page reload. | |
| 46 | 292 | window.ermiteLive = (() => { | |
| 47 | 293 | let reloading = false; | |
| 294 | + | let stopped = false; | |
| 48 | 295 | return { | |
| 49 | 296 | swap(html) { | |
| 50 | 297 | const t = document.createElement('template'); | |
| @@ -57,6 +304,7 @@ window.ermiteLive = (() => { | |||
| 57 | 304 | if (rows && el.id.startsWith('repo-')) { rows.prepend(el); window.htmx?.process(el); } | |
| 58 | 305 | continue; | |
| 59 | 306 | } | |
| 307 | + | if (old.hasAttribute('data-slot')) { old.replaceChildren(...el.childNodes); continue; } | |
| 60 | 308 | if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', ''); | |
| 61 | 309 | if (old.hidden && old.tagName === 'TR') el.hidden = true; | |
| 62 | 310 | old.replaceWith(el); | |
| @@ -68,5 +316,22 @@ window.ermiteLive = (() => { | |||
| 68 | 316 | reloading = true; | |
| 69 | 317 | setTimeout(() => location.reload(), 300); | |
| 70 | 318 | }, | |
| 319 | + | // error handles htmx:sse:error (detail.status is the status of a refused | |
| 320 | + | // reconnection). hx-sse retries for ever; a 404 or a 401 will not change by | |
| 321 | + | // itself, and the fetched page says what is the matter (or has no stream). | |
| 322 | + | // A page that reloaded for this a moment ago and still gets refused would | |
| 323 | + | // loop: it stops following the stream instead (see gate). | |
| 324 | + | error(status) { | |
| 325 | + | if (status !== 404 && status !== 401) return; | |
| 326 | + | let last = 0; | |
| 327 | + | try { last = +sessionStorage.getItem('ermite-live-reload') || 0; } catch { /* no storage: no guard */ } | |
| 328 | + | if (Date.now() - last < 15000) { stopped = true; return; } | |
| 329 | + | try { sessionStorage.setItem('ermite-live-reload', String(Date.now())); } catch { /* idem */ } | |
| 330 | + | this.reload(); | |
| 331 | + | }, | |
| 332 | + | // gate is htmx:sse:before:connection: once stopped, no more attempts. | |
| 333 | + | gate(event) { | |
| 334 | + | if (stopped) event.preventDefault(); | |
| 335 | + | }, | |
| 71 | 336 | }; | |
| 72 | 337 | })(); | |
M internal/adapters/web/tree.templ +2 -2
| @@ -11,10 +11,10 @@ templ Tree(l Layout, p TreePage) { | |||
| 11 | 11 | <input | |
| 12 | 12 | id="tree-filter" | |
| 13 | 13 | type="search" | |
| 14 | - | data-filter | |
| 14 | + | data-filter="#tree-rows > tr" | |
| 15 | 15 | placeholder="Filter this directory…" | |
| 16 | 16 | autocomplete="off" | |
| 17 | - | hx-on:input="let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')" | |
| 17 | + | hx-on:input="ermite.filter(this)" | |
| 18 | 18 | /> | |
| 19 | 19 | </div> | |
| 20 | 20 | <table class="list tree" aria-label="Entries"> | |