detached 0a9e349

#6 feat(web): review page polish and home page ordered by last activity

state
merged · 4ce8534 · squash
branch
feat/mr-header-card → main
opened
bnema ·
updated

Review page and home page polish, plus "last activity" ordering of the repositories.

Merge request page

  • Title, facts and description in one card; the header a shade lighter than the description. Issues share it.
  • Unified / Split / Collapse all in one view menu (the existing details.menu component); u, d and x still work, escape closes menus on every page and the focus goes back to the menu button.
  • Wide mode on every page with w (listed in ?), kept in localStorage; store.js loads without defer and sets it before the first paint.
  • Hunk headers and hidden-line rows are quieter (background and border), text keeps its contrast.
  • A shift + click range is one block (one border, none between its lines, per side in split); the form title names the whole range (a.go:21-149); lines loaded later from a gap inside the range are selected too.

Home page

  • Inbox button next to the logo, home page only, with a mailbox icon (Lucide) and a darker count badge.
  • Default order is the last activity: push (incl. merge), merge request opened / merged / closed / ready, comment on a merge request, issue opened / approved / closed / reopened / commented. Moderation (submissions, rejections) stays owner-only and does not count.
  • The row of a repository with activity moves to the top live (activity order only; CI updates do not move it).

Storage

  • Repo.ActiveAt (storage proto field 8; falls back to the later of pushed and created).
  • RepoMetaStore.Update is replaced by Mutate: a compare-and-swap on the version read, retried up to 8 times. The previous Update checked the version read at write time, so a read-modify-write could overwrite a concurrent change (e.g. a visibility change).

No change to ermite-api.

Checks: go vet ./..., go test ./..., go test -race on usecase and kvstore, JS tests (160), golden pages refreshed, manual checks on the dev server. Reviewed by Go and JS reviewers; findings fixed.

CommitSubjectAuthorDateSignatureStatus
0a9e349test: run the activity store test outside the usecase package and its home row subtests in parallelbriceGPG Verified
9ca7c68fix(repos): mutate metadata atomically and record approved issue activitybriceGPG Verified
32e9fe7fix(web): match loaded lines to a selection by file key, keep the focus on a closing menu, close menus with escape on every page and restore the hunk header contrastbriceGPG Verified
4277264feat(web): order the home page by last activity and move repositories up livebriceGPG Verified
4fc291efeat(web): move the inbox button of the home page next to the logo, with a mailbox icon and a darker count badgebriceGPG Verified
5f6d2c6test(app): refresh the web golden pages for the view menu, the w shortcut and the store scriptbriceGPG Verified
a772c1bfix(web): a selected line range is one block, its title names the whole range and its hidden lines stay selected once loadedbriceGPG Verified
502935erefactor(web): drop the Wide button of the diff bar, the w shortcut covers itbriceGPG Verified
a1f8e85feat(web): make the wide mode a global w shortcut, set before the first paint, and tighten the merge request headerbriceGPG Verified
fcc7703feat(web): one merge request card, a diff view menu, a wide diff mode and quieter hunk rowsbriceGPG Verified

Checks 4

92 files · +2426 -1392
4/4
M gen/ermite/storage/repo.pb.go +22 -11
M internal/adapters/kvstore/issues_test.go +4 -2
M internal/adapters/kvstore/mergerequests_test.go +4 -2
M internal/adapters/kvstore/repos.go +44 -22
A internal/adapters/kvstore/repos_mutate_test.go +87 -0
M internal/adapters/kvstore/store_test.go +8 -4
M internal/adapters/web/components.templ +2 -2
M internal/adapters/web/components_templ.go +119 -113
M internal/adapters/web/diff.templ +22 -4
M internal/adapters/web/diff_templ.go +523 -437
M internal/adapters/web/events.go +2 -0
M internal/adapters/web/format.go +8 -0
M internal/adapters/web/fragments.go +18 -0
M internal/adapters/web/home.go +24 -14
M internal/adapters/web/home_test.go +50 -1
M internal/adapters/web/issues.templ +24 -22
M internal/adapters/web/issues_templ.go +26 -26
M internal/adapters/web/jstest/a11y.test.mjs +25 -0
M internal/adapters/web/keys.templ +1 -0
M internal/adapters/web/keys_templ.go +1 -1
M internal/adapters/web/mr.go +6 -1
M internal/adapters/web/mr.templ +37 -39
M internal/adapters/web/mr_templ.go +107 -123
M internal/adapters/web/overlays.templ +1 -0
M internal/adapters/web/overlays_templ.go +12 -12
M internal/adapters/web/pages.templ +3 -9
@@ -2,6 +2,8 @@
22
33 package web
44
5+import "fmt"
6+
57 // Home is the repository list.
68 templ Home(l Layout, p HomePage) {
79 @Shell(l, nil) {
@@ -9,14 +11,6 @@ templ Home(l Layout, p HomePage) {
911 <div class="home" data-layout="cards">
1012 <section class="home-repos" aria-label="Repositories">
1113 <div class="home-bar">
12- if p.Inbox != nil {
13- <a class="btn inbox-btn" href={ templ.SafeURL(p.Inbox.Href) } title="inbox">
14- Inbox
15- if p.Inbox.Count >= 0 {
16- <span class="count">{ itoa(p.Inbox.Count) }</span>
17- }
18- </a>
19- }
2014 <div class="filter">
2115 <label for="home-filter" class="sr">filter</label>
2216 <input
@@ -57,7 +51,7 @@ templ Home(l Layout, p HomePage) {
5751 </button>
5852 </div>
5953 </div>
60- <ul class="repos" id="repo-rows" data-rows>
54+ <ul class="repos" id="repo-rows" data-rows data-sort={ p.Sort } data-desc={ fmt.Sprint(p.Desc) }>
6155 for _, r := range p.Rows {
6256 @HomeRowView(r)
6357 }
@@ -2,6 +2,8 @@
2 2
3 package web3 package web
4 4
5+import "fmt"
6+
5 // Home is the repository list.7 // Home is the repository list.
6 templ Home(l Layout, p HomePage) {8 templ Home(l Layout, p HomePage) {
7 @Shell(l, nil) {9 @Shell(l, nil) {
@@ -9,14 +11,6 @@ templ Home(l Layout, p HomePage) {
9 <div class="home" data-layout="cards">11 <div class="home" data-layout="cards">
10 <section class="home-repos" aria-label="Repositories">12 <section class="home-repos" aria-label="Repositories">
11 <div class="home-bar">13 <div class="home-bar">
12- if p.Inbox != nil {
13- <a class="btn inbox-btn" href={ templ.SafeURL(p.Inbox.Href) } title="inbox">
14- Inbox
15- if p.Inbox.Count >= 0 {
16- <span class="count">{ itoa(p.Inbox.Count) }</span>
17- }
18- </a>
19- }
20 <div class="filter">14 <div class="filter">
21 <label for="home-filter" class="sr">filter</label>15 <label for="home-filter" class="sr">filter</label>
22 <input16 <input
@@ -57,7 +51,7 @@ templ Home(l Layout, p HomePage) {
57 </button>51 </button>
58 </div>52 </div>
59 </div>53 </div>
60- <ul class="repos" id="repo-rows" data-rows>54+ <ul class="repos" id="repo-rows" data-rows data-sort={ p.Sort } data-desc={ fmt.Sprint(p.Desc) }>
61 for _, r := range p.Rows {55 for _, r := range p.Rows {
62 @HomeRowView(r)56 @HomeRowView(r)
63 }57 }
M internal/adapters/web/pages_templ.go +205 -223 generated file: collapsed
M internal/adapters/web/shell.templ +14 -1
@@ -25,7 +25,7 @@ templ Shell(l Layout, rail templ.Component) {
2525 <script src={ l.Assets.HTMX } defer></script>
2626 <script src={ l.Assets.Live } defer></script>
2727 <script src={ l.Assets.SSE } defer></script>
28- <script src={ l.Assets.Store } defer></script>
28+ <script src={ l.Assets.Store }></script>
2929 <script src={ l.Assets.App } defer></script>
3030 <script src={ l.Assets.Files } defer></script>
3131 <script src={ l.Assets.Passkey } defer></script>
@@ -80,6 +80,19 @@ templ Header(l Layout) {
8080 <a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
8181 <span class="brand-mark" aria-hidden="true"></span>
8282 </a>
83+ if l.Inbox != nil {
84+ <a
85+ class="btn inbox-btn"
86+ href={ templ.SafeURL(l.Inbox.Href) }
87+ title="inbox"
88+ >
89+ <span class="ico i-mailbox" aria-hidden="true"></span>
90+ Inbox
91+ if l.Inbox.Count >= 0 {
92+ <span class="count">{ itoa(l.Inbox.Count) }</span>
93+ }
94+ </a>
95+ }
8396 if len(l.Crumbs) > 0 {
8497 <nav class="crumbs" aria-label="Path">
8598 <ol>
@@ -25,7 +25,7 @@ templ Shell(l Layout, rail templ.Component) {
25 <script src={ l.Assets.HTMX } defer></script>25 <script src={ l.Assets.HTMX } defer></script>
26 <script src={ l.Assets.Live } defer></script>26 <script src={ l.Assets.Live } defer></script>
27 <script src={ l.Assets.SSE } defer></script>27 <script src={ l.Assets.SSE } defer></script>
28- <script src={ l.Assets.Store } defer></script>28+ <script src={ l.Assets.Store }></script>
29 <script src={ l.Assets.App } defer></script>29 <script src={ l.Assets.App } defer></script>
30 <script src={ l.Assets.Files } defer></script>30 <script src={ l.Assets.Files } defer></script>
31 <script src={ l.Assets.Passkey } defer></script>31 <script src={ l.Assets.Passkey } defer></script>
@@ -80,6 +80,19 @@ templ Header(l Layout) {
80 <a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">80 <a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
81 <span class="brand-mark" aria-hidden="true"></span>81 <span class="brand-mark" aria-hidden="true"></span>
82 </a>82 </a>
83+ if l.Inbox != nil {
84+ <a
85+ class="btn inbox-btn"
86+ href={ templ.SafeURL(l.Inbox.Href) }
87+ title="inbox"
88+ >
89+ <span class="ico i-mailbox" aria-hidden="true"></span>
90+ Inbox
91+ if l.Inbox.Count >= 0 {
92+ <span class="count">{ itoa(l.Inbox.Count) }</span>
93+ }
94+ </a>
95+ }
83 if len(l.Crumbs) > 0 {96 if len(l.Crumbs) > 0 {
84 <nav class="crumbs" aria-label="Path">97 <nav class="crumbs" aria-label="Path">
85 <ol>98 <ol>
M internal/adapters/web/shell_templ.go +158 -116
@@ -164,7 +164,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
164164 if templ_7745c5c3_Err != nil {
165165 return templ_7745c5c3_Err
166166 }
167- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" defer></script><script src=\"")
167+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"></script><script src=\"")
168168 if templ_7745c5c3_Err != nil {
169169 return templ_7745c5c3_Err
170170 }
@@ -458,315 +458,357 @@ func Header(l Layout) templ.Component {
458458 if templ_7745c5c3_Err != nil {
459459 return templ_7745c5c3_Err
460460 }
461+ if l.Inbox != nil {
462+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<a class=\"btn inbox-btn\" href=\"")
463+ if templ_7745c5c3_Err != nil {
464+ return templ_7745c5c3_Err
465+ }
466+ var templ_7745c5c3_Var26 templ.SafeURL
467+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Inbox.Href))
468+ if templ_7745c5c3_Err != nil {
469+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 86, Col: 39}
470+ }
471+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
472+ if templ_7745c5c3_Err != nil {
473+ return templ_7745c5c3_Err
474+ }
475+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" title=\"inbox\"><span class=\"ico i-mailbox\" aria-hidden=\"true\"></span> Inbox ")
476+ if templ_7745c5c3_Err != nil {
477+ return templ_7745c5c3_Err
478+ }
479+ if l.Inbox.Count >= 0 {
480+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<span class=\"count\">")
481+ if templ_7745c5c3_Err != nil {
482+ return templ_7745c5c3_Err
483+ }
484+ var templ_7745c5c3_Var27 string
485+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(l.Inbox.Count))
486+ if templ_7745c5c3_Err != nil {
487+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 92, Col: 47}
488+ }
489+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
490+ if templ_7745c5c3_Err != nil {
491+ return templ_7745c5c3_Err
492+ }
493+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span>")
494+ if templ_7745c5c3_Err != nil {
495+ return templ_7745c5c3_Err
496+ }
497+ }
498+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</a> ")
499+ if templ_7745c5c3_Err != nil {
500+ return templ_7745c5c3_Err
501+ }
502+ }
461503 if len(l.Crumbs) > 0 {
462- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
504+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
463505 if templ_7745c5c3_Err != nil {
464506 return templ_7745c5c3_Err
465507 }
466508 for _, c := range l.Crumbs {
467- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<li>")
509+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<li>")
468510 if templ_7745c5c3_Err != nil {
469511 return templ_7745c5c3_Err
470512 }
471513 if c.Href != "" {
472- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<a href=\"")
514+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<a href=\"")
473515 if templ_7745c5c3_Err != nil {
474516 return templ_7745c5c3_Err
475517 }
476- var templ_7745c5c3_Var26 templ.SafeURL
477- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
518+ var templ_7745c5c3_Var28 templ.SafeURL
519+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
478520 if templ_7745c5c3_Err != nil {
479- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 90, Col: 38}
521+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 103, Col: 38}
480522 }
481- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
523+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
482524 if templ_7745c5c3_Err != nil {
483525 return templ_7745c5c3_Err
484526 }
485- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"")
527+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"")
486528 if templ_7745c5c3_Err != nil {
487529 return templ_7745c5c3_Err
488530 }
489531 if c.Current {
490- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " aria-current=\"page\"")
532+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " aria-current=\"page\"")
491533 if templ_7745c5c3_Err != nil {
492534 return templ_7745c5c3_Err
493535 }
494536 }
495- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">")
537+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, ">")
496538 if templ_7745c5c3_Err != nil {
497539 return templ_7745c5c3_Err
498540 }
499- var templ_7745c5c3_Var27 string
500- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
541+ var templ_7745c5c3_Var29 string
542+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
501543 if templ_7745c5c3_Err != nil {
502- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 94, Col: 19}
544+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 107, Col: 19}
503545 }
504- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
546+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
505547 if templ_7745c5c3_Err != nil {
506548 return templ_7745c5c3_Err
507549 }
508- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a>")
550+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</a>")
509551 if templ_7745c5c3_Err != nil {
510552 return templ_7745c5c3_Err
511553 }
512554 } else {
513- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<span")
555+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<span")
514556 if templ_7745c5c3_Err != nil {
515557 return templ_7745c5c3_Err
516558 }
517559 if c.Current {
518- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " aria-current=\"page\"")
560+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " aria-current=\"page\"")
519561 if templ_7745c5c3_Err != nil {
520562 return templ_7745c5c3_Err
521563 }
522564 }
523- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">")
565+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, ">")
524566 if templ_7745c5c3_Err != nil {
525567 return templ_7745c5c3_Err
526568 }
527- var templ_7745c5c3_Var28 string
528- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
569+ var templ_7745c5c3_Var30 string
570+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
529571 if templ_7745c5c3_Err != nil {
530- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 100, Col: 19}
572+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 113, Col: 19}
531573 }
532- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
574+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
533575 if templ_7745c5c3_Err != nil {
534576 return templ_7745c5c3_Err
535577 }
536- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span>")
578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span>")
537579 if templ_7745c5c3_Err != nil {
538580 return templ_7745c5c3_Err
539581 }
540582 }
541- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</li>")
583+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</li>")
542584 if templ_7745c5c3_Err != nil {
543585 return templ_7745c5c3_Err
544586 }
545587 }
546- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</ol></nav>")
588+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</ol></nav>")
547589 if templ_7745c5c3_Err != nil {
548590 return templ_7745c5c3_Err
549591 }
550592 }
551- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div class=\"hdr-acts\">")
593+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<div class=\"hdr-acts\">")
552594 if templ_7745c5c3_Err != nil {
553595 return templ_7745c5c3_Err
554596 }
555597 if l.Ref != "" {
556- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<a class=\"btn ref\" href=\"")
598+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<a class=\"btn ref\" href=\"")
557599 if templ_7745c5c3_Err != nil {
558600 return templ_7745c5c3_Err
559601 }
560- var templ_7745c5c3_Var29 templ.SafeURL
561- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
602+ var templ_7745c5c3_Var31 templ.SafeURL
603+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
562604 if templ_7745c5c3_Err != nil {
563- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 111, Col: 52}
605+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 124, Col: 52}
564606 }
565- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
607+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
566608 if templ_7745c5c3_Err != nil {
567609 return templ_7745c5c3_Err
568610 }
569- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
611+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
570612 if templ_7745c5c3_Err != nil {
571613 return templ_7745c5c3_Err
572614 }
573- var templ_7745c5c3_Var30 string
574- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
615+ var templ_7745c5c3_Var32 string
616+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
575617 if templ_7745c5c3_Err != nil {
576- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 117, Col: 36}
618+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 130, Col: 36}
577619 }
578- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
620+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
579621 if templ_7745c5c3_Err != nil {
580622 return templ_7745c5c3_Err
581623 }
582- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</span> <span class=\"sha\">")
624+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span> <span class=\"sha\">")
583625 if templ_7745c5c3_Err != nil {
584626 return templ_7745c5c3_Err
585627 }
586- var templ_7745c5c3_Var31 string
587- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
628+ var templ_7745c5c3_Var33 string
629+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
588630 if templ_7745c5c3_Err != nil {
589- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 118, Col: 33}
631+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 131, Col: 33}
590632 }
591- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
633+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
592634 if templ_7745c5c3_Err != nil {
593635 return templ_7745c5c3_Err
594636 }
595- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span></a> ")
637+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</span></a> ")
596638 if templ_7745c5c3_Err != nil {
597639 return templ_7745c5c3_Err
598640 }
599641 }
600- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")
642+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")
601643 if templ_7745c5c3_Err != nil {
602644 return templ_7745c5c3_Err
603645 }
604646 if SignedIn(ctx) {
605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")
647+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")
606648 if templ_7745c5c3_Err != nil {
607649 return templ_7745c5c3_Err
608650 }
609- var templ_7745c5c3_Var32 string
610- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
651+ var templ_7745c5c3_Var34 string
652+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
611653 if templ_7745c5c3_Err != nil {
612- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 137, Col: 123}
654+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 150, Col: 123}
613655 }
614- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
656+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
615657 if templ_7745c5c3_Err != nil {
616658 return templ_7745c5c3_Err
617659 }
618- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span> ")
660+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</span> ")
619661 if templ_7745c5c3_Err != nil {
620662 return templ_7745c5c3_Err
621663 }
622664 if l.Passkeys {
623- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")
665+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")
624666 if templ_7745c5c3_Err != nil {
625667 return templ_7745c5c3_Err
626668 }
627669 }
628- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")
670+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")
629671 if templ_7745c5c3_Err != nil {
630672 return templ_7745c5c3_Err
631673 }
632674 } else if l.Passkeys {
633- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")
675+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")
634676 if templ_7745c5c3_Err != nil {
635677 return templ_7745c5c3_Err
636678 }
637679 }
638- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</div></div>")
680+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</div></div>")
639681 if templ_7745c5c3_Err != nil {
640682 return templ_7745c5c3_Err
641683 }
642684 if len(l.Sections) > 0 {
643- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
644686 if templ_7745c5c3_Err != nil {
645687 return templ_7745c5c3_Err
646688 }
647689 for _, s := range l.Sections {
648- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<li><a href=\"")
690+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<li><a href=\"")
649691 if templ_7745c5c3_Err != nil {
650692 return templ_7745c5c3_Err
651693 }
652- var templ_7745c5c3_Var33 templ.SafeURL
653- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
694+ var templ_7745c5c3_Var35 templ.SafeURL
695+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
654696 if templ_7745c5c3_Err != nil {
655- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 154, Col: 36}
697+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 167, Col: 36}
656698 }
657- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
699+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
658700 if templ_7745c5c3_Err != nil {
659701 return templ_7745c5c3_Err
660702 }
661- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" data-key=\"")
703+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" data-key=\"")
662704 if templ_7745c5c3_Err != nil {
663705 return templ_7745c5c3_Err
664706 }
665- var templ_7745c5c3_Var34 string
666- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
707+ var templ_7745c5c3_Var36 string
708+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
667709 if templ_7745c5c3_Err != nil {
668- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 155, Col: 24}
710+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 168, Col: 24}
669711 }
670- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
712+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
671713 if templ_7745c5c3_Err != nil {
672714 return templ_7745c5c3_Err
673715 }
674- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\" aria-keyshortcuts=\"")
716+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\" aria-keyshortcuts=\"")
675717 if templ_7745c5c3_Err != nil {
676718 return templ_7745c5c3_Err
677719 }
678- var templ_7745c5c3_Var35 string
679- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
720+ var templ_7745c5c3_Var37 string
721+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
680722 if templ_7745c5c3_Err != nil {
681- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 156, Col: 33}
723+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 169, Col: 33}
682724 }
683- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
725+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
684726 if templ_7745c5c3_Err != nil {
685727 return templ_7745c5c3_Err
686728 }
687- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\"")
729+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\"")
688730 if templ_7745c5c3_Err != nil {
689731 return templ_7745c5c3_Err
690732 }
691733 if s.Current {
692- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " aria-current=\"page\"")
734+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " aria-current=\"page\"")
693735 if templ_7745c5c3_Err != nil {
694736 return templ_7745c5c3_Err
695737 }
696738 }
697- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, ">")
739+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, ">")
698740 if templ_7745c5c3_Err != nil {
699741 return templ_7745c5c3_Err
700742 }
701- var templ_7745c5c3_Var36 string
702- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
743+ var templ_7745c5c3_Var38 string
744+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
703745 if templ_7745c5c3_Err != nil {
704- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 161, Col: 17}
746+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 174, Col: 17}
705747 }
706- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
748+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
707749 if templ_7745c5c3_Err != nil {
708750 return templ_7745c5c3_Err
709751 }
710- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " ")
752+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " ")
711753 if templ_7745c5c3_Err != nil {
712754 return templ_7745c5c3_Err
713755 }
714756 if s.Count != "" {
715- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<span class=\"count\">")
757+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<span class=\"count\">")
716758 if templ_7745c5c3_Err != nil {
717759 return templ_7745c5c3_Err
718760 }
719- var templ_7745c5c3_Var37 string
720- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
761+ var templ_7745c5c3_Var39 string
762+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
721763 if templ_7745c5c3_Err != nil {
722- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 163, Col: 38}
764+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 38}
723765 }
724- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
766+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
725767 if templ_7745c5c3_Err != nil {
726768 return templ_7745c5c3_Err
727769 }
728- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<span class=\"sr\">")
770+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<span class=\"sr\">")
729771 if templ_7745c5c3_Err != nil {
730772 return templ_7745c5c3_Err
731773 }
732- var templ_7745c5c3_Var38 string
733- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
774+ var templ_7745c5c3_Var40 string
775+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
734776 if templ_7745c5c3_Err != nil {
735- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 163, Col: 66}
777+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 66}
736778 }
737- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
779+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
738780 if templ_7745c5c3_Err != nil {
739781 return templ_7745c5c3_Err
740782 }
741- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</span></span> ")
783+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</span></span> ")
742784 if templ_7745c5c3_Err != nil {
743785 return templ_7745c5c3_Err
744786 }
745787 }
746- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<kbd aria-hidden=\"true\">")
788+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<kbd aria-hidden=\"true\">")
747789 if templ_7745c5c3_Err != nil {
748790 return templ_7745c5c3_Err
749791 }
750- var templ_7745c5c3_Var39 string
751- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
792+ var templ_7745c5c3_Var41 string
793+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
752794 if templ_7745c5c3_Err != nil {
753- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 165, Col: 39}
795+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 178, Col: 39}
754796 }
755- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
797+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
756798 if templ_7745c5c3_Err != nil {
757799 return templ_7745c5c3_Err
758800 }
759- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</kbd></a></li>")
801+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</kbd></a></li>")
760802 if templ_7745c5c3_Err != nil {
761803 return templ_7745c5c3_Err
762804 }
763805 }
764- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</ul></nav>")
806+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</ul></nav>")
765807 if templ_7745c5c3_Err != nil {
766808 return templ_7745c5c3_Err
767809 }
768810 }
769- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</header>")
811+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</header>")
770812 if templ_7745c5c3_Err != nil {
771813 return templ_7745c5c3_Err
772814 }
@@ -792,38 +834,38 @@ func Footer(l Layout) templ.Component {
792834 }()
793835 }
794836 ctx = templ.InitializeContext(ctx)
795- templ_7745c5c3_Var40 := templ.GetChildren(ctx)
796- if templ_7745c5c3_Var40 == nil {
797- templ_7745c5c3_Var40 = templ.NopComponent
837+ templ_7745c5c3_Var42 := templ.GetChildren(ctx)
838+ if templ_7745c5c3_Var42 == nil {
839+ templ_7745c5c3_Var42 = templ.NopComponent
798840 }
799841 ctx = templ.ClearChildren(ctx)
800- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<footer class=\"foot\"><span>ermite ")
842+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<footer class=\"foot\"><span>ermite ")
801843 if templ_7745c5c3_Err != nil {
802844 return templ_7745c5c3_Err
803845 }
804- var templ_7745c5c3_Var41 string
805- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
846+ var templ_7745c5c3_Var43 string
847+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
806848 if templ_7745c5c3_Err != nil {
807- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 26}
849+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 192, Col: 26}
808850 }
809- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
851+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
810852 if templ_7745c5c3_Err != nil {
811853 return templ_7745c5c3_Err
812854 }
813- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</span> <a href=\"")
855+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</span> <a href=\"")
814856 if templ_7745c5c3_Err != nil {
815857 return templ_7745c5c3_Err
816858 }
817- var templ_7745c5c3_Var42 templ.SafeURL
818- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
859+ var templ_7745c5c3_Var44 templ.SafeURL
860+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
819861 if templ_7745c5c3_Err != nil {
820- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 180, Col: 38}
862+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 193, Col: 38}
821863 }
822- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
864+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
823865 if templ_7745c5c3_Err != nil {
824866 return templ_7745c5c3_Err
825867 }
826- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
868+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
827869 if templ_7745c5c3_Err != nil {
828870 return templ_7745c5c3_Err
829871 }
@@ -851,12 +893,12 @@ func Hint(l Layout) templ.Component {
851893 }()
852894 }
853895 ctx = templ.InitializeContext(ctx)
854- templ_7745c5c3_Var43 := templ.GetChildren(ctx)
855- if templ_7745c5c3_Var43 == nil {
856- templ_7745c5c3_Var43 = templ.NopComponent
896+ templ_7745c5c3_Var45 := templ.GetChildren(ctx)
897+ if templ_7745c5c3_Var45 == nil {
898+ templ_7745c5c3_Var45 = templ.NopComponent
857899 }
858900 ctx = templ.ClearChildren(ctx)
859- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
901+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
860902 if templ_7745c5c3_Err != nil {
861903 return templ_7745c5c3_Err
862904 }
@@ -864,7 +906,7 @@ func Hint(l Layout) templ.Component {
864906 if templ_7745c5c3_Err != nil {
865907 return templ_7745c5c3_Err
866908 }
867- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</div>")
909+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</div>")
868910 if templ_7745c5c3_Err != nil {
869911 return templ_7745c5c3_Err
870912 }
@@ -164,7 +164,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
164 if templ_7745c5c3_Err != nil {164 if templ_7745c5c3_Err != nil {
165 return templ_7745c5c3_Err165 return templ_7745c5c3_Err
166 }166 }
167- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" defer></script><script src=\"")167+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"></script><script src=\"")
168 if templ_7745c5c3_Err != nil {168 if templ_7745c5c3_Err != nil {
169 return templ_7745c5c3_Err169 return templ_7745c5c3_Err
170 }170 }
@@ -458,315 +458,357 @@ func Header(l Layout) templ.Component {
458 if templ_7745c5c3_Err != nil {458 if templ_7745c5c3_Err != nil {
459 return templ_7745c5c3_Err459 return templ_7745c5c3_Err
460 }460 }
461+ if l.Inbox != nil {
462+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<a class=\"btn inbox-btn\" href=\"")
463+ if templ_7745c5c3_Err != nil {
464+ return templ_7745c5c3_Err
465+ }
466+ var templ_7745c5c3_Var26 templ.SafeURL
467+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Inbox.Href))
468+ if templ_7745c5c3_Err != nil {
469+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 86, Col: 39}
470+ }
471+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
472+ if templ_7745c5c3_Err != nil {
473+ return templ_7745c5c3_Err
474+ }
475+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" title=\"inbox\"><span class=\"ico i-mailbox\" aria-hidden=\"true\"></span> Inbox ")
476+ if templ_7745c5c3_Err != nil {
477+ return templ_7745c5c3_Err
478+ }
479+ if l.Inbox.Count >= 0 {
480+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<span class=\"count\">")
481+ if templ_7745c5c3_Err != nil {
482+ return templ_7745c5c3_Err
483+ }
484+ var templ_7745c5c3_Var27 string
485+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(l.Inbox.Count))
486+ if templ_7745c5c3_Err != nil {
487+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 92, Col: 47}
488+ }
489+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
490+ if templ_7745c5c3_Err != nil {
491+ return templ_7745c5c3_Err
492+ }
493+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span>")
494+ if templ_7745c5c3_Err != nil {
495+ return templ_7745c5c3_Err
496+ }
497+ }
498+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</a> ")
499+ if templ_7745c5c3_Err != nil {
500+ return templ_7745c5c3_Err
501+ }
502+ }
461 if len(l.Crumbs) > 0 {503 if len(l.Crumbs) > 0 {
462- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")504+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
463 if templ_7745c5c3_Err != nil {505 if templ_7745c5c3_Err != nil {
464 return templ_7745c5c3_Err506 return templ_7745c5c3_Err
465 }507 }
466 for _, c := range l.Crumbs {508 for _, c := range l.Crumbs {
467- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<li>")509+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<li>")
468 if templ_7745c5c3_Err != nil {510 if templ_7745c5c3_Err != nil {
469 return templ_7745c5c3_Err511 return templ_7745c5c3_Err
470 }512 }
471 if c.Href != "" {513 if c.Href != "" {
472- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<a href=\"")514+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<a href=\"")
473 if templ_7745c5c3_Err != nil {515 if templ_7745c5c3_Err != nil {
474 return templ_7745c5c3_Err516 return templ_7745c5c3_Err
475 }517 }
476- var templ_7745c5c3_Var26 templ.SafeURL518+ var templ_7745c5c3_Var28 templ.SafeURL
477- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))519+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
478 if templ_7745c5c3_Err != nil {520 if templ_7745c5c3_Err != nil {
479- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 90, Col: 38}521+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 103, Col: 38}
480 }522 }
481- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))523+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
482 if templ_7745c5c3_Err != nil {524 if templ_7745c5c3_Err != nil {
483 return templ_7745c5c3_Err525 return templ_7745c5c3_Err
484 }526 }
485- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"")527+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"")
486 if templ_7745c5c3_Err != nil {528 if templ_7745c5c3_Err != nil {
487 return templ_7745c5c3_Err529 return templ_7745c5c3_Err
488 }530 }
489 if c.Current {531 if c.Current {
490- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " aria-current=\"page\"")532+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " aria-current=\"page\"")
491 if templ_7745c5c3_Err != nil {533 if templ_7745c5c3_Err != nil {
492 return templ_7745c5c3_Err534 return templ_7745c5c3_Err
493 }535 }
494 }536 }
495- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">")537+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, ">")
496 if templ_7745c5c3_Err != nil {538 if templ_7745c5c3_Err != nil {
497 return templ_7745c5c3_Err539 return templ_7745c5c3_Err
498 }540 }
499- var templ_7745c5c3_Var27 string541+ var templ_7745c5c3_Var29 string
500- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)542+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
501 if templ_7745c5c3_Err != nil {543 if templ_7745c5c3_Err != nil {
502- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 94, Col: 19}544+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 107, Col: 19}
503 }545 }
504- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))546+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
505 if templ_7745c5c3_Err != nil {547 if templ_7745c5c3_Err != nil {
506 return templ_7745c5c3_Err548 return templ_7745c5c3_Err
507 }549 }
508- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a>")550+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</a>")
509 if templ_7745c5c3_Err != nil {551 if templ_7745c5c3_Err != nil {
510 return templ_7745c5c3_Err552 return templ_7745c5c3_Err
511 }553 }
512 } else {554 } else {
513- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<span")555+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<span")
514 if templ_7745c5c3_Err != nil {556 if templ_7745c5c3_Err != nil {
515 return templ_7745c5c3_Err557 return templ_7745c5c3_Err
516 }558 }
517 if c.Current {559 if c.Current {
518- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " aria-current=\"page\"")560+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " aria-current=\"page\"")
519 if templ_7745c5c3_Err != nil {561 if templ_7745c5c3_Err != nil {
520 return templ_7745c5c3_Err562 return templ_7745c5c3_Err
521 }563 }
522 }564 }
523- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">")565+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, ">")
524 if templ_7745c5c3_Err != nil {566 if templ_7745c5c3_Err != nil {
525 return templ_7745c5c3_Err567 return templ_7745c5c3_Err
526 }568 }
527- var templ_7745c5c3_Var28 string569+ var templ_7745c5c3_Var30 string
528- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)570+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
529 if templ_7745c5c3_Err != nil {571 if templ_7745c5c3_Err != nil {
530- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 100, Col: 19}572+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 113, Col: 19}
531 }573 }
532- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))574+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
533 if templ_7745c5c3_Err != nil {575 if templ_7745c5c3_Err != nil {
534 return templ_7745c5c3_Err576 return templ_7745c5c3_Err
535 }577 }
536- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span>")578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span>")
537 if templ_7745c5c3_Err != nil {579 if templ_7745c5c3_Err != nil {
538 return templ_7745c5c3_Err580 return templ_7745c5c3_Err
539 }581 }
540 }582 }
541- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</li>")583+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</li>")
542 if templ_7745c5c3_Err != nil {584 if templ_7745c5c3_Err != nil {
543 return templ_7745c5c3_Err585 return templ_7745c5c3_Err
544 }586 }
545 }587 }
546- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</ol></nav>")588+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</ol></nav>")
547 if templ_7745c5c3_Err != nil {589 if templ_7745c5c3_Err != nil {
548 return templ_7745c5c3_Err590 return templ_7745c5c3_Err
549 }591 }
550 }592 }
551- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div class=\"hdr-acts\">")593+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<div class=\"hdr-acts\">")
552 if templ_7745c5c3_Err != nil {594 if templ_7745c5c3_Err != nil {
553 return templ_7745c5c3_Err595 return templ_7745c5c3_Err
554 }596 }
555 if l.Ref != "" {597 if l.Ref != "" {
556- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<a class=\"btn ref\" href=\"")598+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<a class=\"btn ref\" href=\"")
557 if templ_7745c5c3_Err != nil {599 if templ_7745c5c3_Err != nil {
558 return templ_7745c5c3_Err600 return templ_7745c5c3_Err
559 }601 }
560- var templ_7745c5c3_Var29 templ.SafeURL602+ var templ_7745c5c3_Var31 templ.SafeURL
561- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))603+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
562 if templ_7745c5c3_Err != nil {604 if templ_7745c5c3_Err != nil {
563- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 111, Col: 52}605+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 124, Col: 52}
564 }606 }
565- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))607+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
566 if templ_7745c5c3_Err != nil {608 if templ_7745c5c3_Err != nil {
567 return templ_7745c5c3_Err609 return templ_7745c5c3_Err
568 }610 }
569- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")611+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
570 if templ_7745c5c3_Err != nil {612 if templ_7745c5c3_Err != nil {
571 return templ_7745c5c3_Err613 return templ_7745c5c3_Err
572 }614 }
573- var templ_7745c5c3_Var30 string615+ var templ_7745c5c3_Var32 string
574- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)616+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
575 if templ_7745c5c3_Err != nil {617 if templ_7745c5c3_Err != nil {
576- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 117, Col: 36}618+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 130, Col: 36}
577 }619 }
578- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))620+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
579 if templ_7745c5c3_Err != nil {621 if templ_7745c5c3_Err != nil {
580 return templ_7745c5c3_Err622 return templ_7745c5c3_Err
581 }623 }
582- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</span> <span class=\"sha\">")624+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span> <span class=\"sha\">")
583 if templ_7745c5c3_Err != nil {625 if templ_7745c5c3_Err != nil {
584 return templ_7745c5c3_Err626 return templ_7745c5c3_Err
585 }627 }
586- var templ_7745c5c3_Var31 string628+ var templ_7745c5c3_Var33 string
587- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)629+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
588 if templ_7745c5c3_Err != nil {630 if templ_7745c5c3_Err != nil {
589- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 118, Col: 33}631+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 131, Col: 33}
590 }632 }
591- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))633+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
592 if templ_7745c5c3_Err != nil {634 if templ_7745c5c3_Err != nil {
593 return templ_7745c5c3_Err635 return templ_7745c5c3_Err
594 }636 }
595- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span></a> ")637+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</span></a> ")
596 if templ_7745c5c3_Err != nil {638 if templ_7745c5c3_Err != nil {
597 return templ_7745c5c3_Err639 return templ_7745c5c3_Err
598 }640 }
599 }641 }
600- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")642+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")
601 if templ_7745c5c3_Err != nil {643 if templ_7745c5c3_Err != nil {
602 return templ_7745c5c3_Err644 return templ_7745c5c3_Err
603 }645 }
604 if SignedIn(ctx) {646 if SignedIn(ctx) {
605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")647+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")
606 if templ_7745c5c3_Err != nil {648 if templ_7745c5c3_Err != nil {
607 return templ_7745c5c3_Err649 return templ_7745c5c3_Err
608 }650 }
609- var templ_7745c5c3_Var32 string651+ var templ_7745c5c3_Var34 string
610- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)652+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
611 if templ_7745c5c3_Err != nil {653 if templ_7745c5c3_Err != nil {
612- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 137, Col: 123}654+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 150, Col: 123}
613 }655 }
614- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))656+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
615 if templ_7745c5c3_Err != nil {657 if templ_7745c5c3_Err != nil {
616 return templ_7745c5c3_Err658 return templ_7745c5c3_Err
617 }659 }
618- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span> ")660+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</span> ")
619 if templ_7745c5c3_Err != nil {661 if templ_7745c5c3_Err != nil {
620 return templ_7745c5c3_Err662 return templ_7745c5c3_Err
621 }663 }
622 if l.Passkeys {664 if l.Passkeys {
623- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")665+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")
624 if templ_7745c5c3_Err != nil {666 if templ_7745c5c3_Err != nil {
625 return templ_7745c5c3_Err667 return templ_7745c5c3_Err
626 }668 }
627 }669 }
628- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")670+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")
629 if templ_7745c5c3_Err != nil {671 if templ_7745c5c3_Err != nil {
630 return templ_7745c5c3_Err672 return templ_7745c5c3_Err
631 }673 }
632 } else if l.Passkeys {674 } else if l.Passkeys {
633- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")675+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")
634 if templ_7745c5c3_Err != nil {676 if templ_7745c5c3_Err != nil {
635 return templ_7745c5c3_Err677 return templ_7745c5c3_Err
636 }678 }
637 }679 }
638- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</div></div>")680+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</div></div>")
639 if templ_7745c5c3_Err != nil {681 if templ_7745c5c3_Err != nil {
640 return templ_7745c5c3_Err682 return templ_7745c5c3_Err
641 }683 }
642 if len(l.Sections) > 0 {684 if len(l.Sections) > 0 {
643- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
644 if templ_7745c5c3_Err != nil {686 if templ_7745c5c3_Err != nil {
645 return templ_7745c5c3_Err687 return templ_7745c5c3_Err
646 }688 }
647 for _, s := range l.Sections {689 for _, s := range l.Sections {
648- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<li><a href=\"")690+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<li><a href=\"")
649 if templ_7745c5c3_Err != nil {691 if templ_7745c5c3_Err != nil {
650 return templ_7745c5c3_Err692 return templ_7745c5c3_Err
651 }693 }
652- var templ_7745c5c3_Var33 templ.SafeURL694+ var templ_7745c5c3_Var35 templ.SafeURL
653- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))695+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
654 if templ_7745c5c3_Err != nil {696 if templ_7745c5c3_Err != nil {
655- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 154, Col: 36}697+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 167, Col: 36}
656 }698 }
657- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))699+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
658 if templ_7745c5c3_Err != nil {700 if templ_7745c5c3_Err != nil {
659 return templ_7745c5c3_Err701 return templ_7745c5c3_Err
660 }702 }
661- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" data-key=\"")703+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" data-key=\"")
662 if templ_7745c5c3_Err != nil {704 if templ_7745c5c3_Err != nil {
663 return templ_7745c5c3_Err705 return templ_7745c5c3_Err
664 }706 }
665- var templ_7745c5c3_Var34 string707+ var templ_7745c5c3_Var36 string
666- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)708+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
667 if templ_7745c5c3_Err != nil {709 if templ_7745c5c3_Err != nil {
668- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 155, Col: 24}710+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 168, Col: 24}
669 }711 }
670- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)712+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
671 if templ_7745c5c3_Err != nil {713 if templ_7745c5c3_Err != nil {
672 return templ_7745c5c3_Err714 return templ_7745c5c3_Err
673 }715 }
674- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\" aria-keyshortcuts=\"")716+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\" aria-keyshortcuts=\"")
675 if templ_7745c5c3_Err != nil {717 if templ_7745c5c3_Err != nil {
676 return templ_7745c5c3_Err718 return templ_7745c5c3_Err
677 }719 }
678- var templ_7745c5c3_Var35 string720+ var templ_7745c5c3_Var37 string
679- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)721+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
680 if templ_7745c5c3_Err != nil {722 if templ_7745c5c3_Err != nil {
681- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 156, Col: 33}723+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 169, Col: 33}
682 }724 }
683- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)725+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
684 if templ_7745c5c3_Err != nil {726 if templ_7745c5c3_Err != nil {
685 return templ_7745c5c3_Err727 return templ_7745c5c3_Err
686 }728 }
687- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\"")729+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\"")
688 if templ_7745c5c3_Err != nil {730 if templ_7745c5c3_Err != nil {
689 return templ_7745c5c3_Err731 return templ_7745c5c3_Err
690 }732 }
691 if s.Current {733 if s.Current {
692- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " aria-current=\"page\"")734+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " aria-current=\"page\"")
693 if templ_7745c5c3_Err != nil {735 if templ_7745c5c3_Err != nil {
694 return templ_7745c5c3_Err736 return templ_7745c5c3_Err
695 }737 }
696 }738 }
697- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, ">")739+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, ">")
698 if templ_7745c5c3_Err != nil {740 if templ_7745c5c3_Err != nil {
699 return templ_7745c5c3_Err741 return templ_7745c5c3_Err
700 }742 }
701- var templ_7745c5c3_Var36 string743+ var templ_7745c5c3_Var38 string
702- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)744+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
703 if templ_7745c5c3_Err != nil {745 if templ_7745c5c3_Err != nil {
704- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 161, Col: 17}746+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 174, Col: 17}
705 }747 }
706- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))748+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
707 if templ_7745c5c3_Err != nil {749 if templ_7745c5c3_Err != nil {
708 return templ_7745c5c3_Err750 return templ_7745c5c3_Err
709 }751 }
710- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " ")752+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " ")
711 if templ_7745c5c3_Err != nil {753 if templ_7745c5c3_Err != nil {
712 return templ_7745c5c3_Err754 return templ_7745c5c3_Err
713 }755 }
714 if s.Count != "" {756 if s.Count != "" {
715- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<span class=\"count\">")757+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<span class=\"count\">")
716 if templ_7745c5c3_Err != nil {758 if templ_7745c5c3_Err != nil {
717 return templ_7745c5c3_Err759 return templ_7745c5c3_Err
718 }760 }
719- var templ_7745c5c3_Var37 string761+ var templ_7745c5c3_Var39 string
720- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)762+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
721 if templ_7745c5c3_Err != nil {763 if templ_7745c5c3_Err != nil {
722- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 163, Col: 38}764+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 38}
723 }765 }
724- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))766+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
725 if templ_7745c5c3_Err != nil {767 if templ_7745c5c3_Err != nil {
726 return templ_7745c5c3_Err768 return templ_7745c5c3_Err
727 }769 }
728- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<span class=\"sr\">")770+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<span class=\"sr\">")
729 if templ_7745c5c3_Err != nil {771 if templ_7745c5c3_Err != nil {
730 return templ_7745c5c3_Err772 return templ_7745c5c3_Err
731 }773 }
732- var templ_7745c5c3_Var38 string774+ var templ_7745c5c3_Var40 string
733- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(" open")775+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
734 if templ_7745c5c3_Err != nil {776 if templ_7745c5c3_Err != nil {
735- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 163, Col: 66}777+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 66}
736 }778 }
737- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))779+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
738 if templ_7745c5c3_Err != nil {780 if templ_7745c5c3_Err != nil {
739 return templ_7745c5c3_Err781 return templ_7745c5c3_Err
740 }782 }
741- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</span></span> ")783+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</span></span> ")
742 if templ_7745c5c3_Err != nil {784 if templ_7745c5c3_Err != nil {
743 return templ_7745c5c3_Err785 return templ_7745c5c3_Err
744 }786 }
745 }787 }
746- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<kbd aria-hidden=\"true\">")788+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<kbd aria-hidden=\"true\">")
747 if templ_7745c5c3_Err != nil {789 if templ_7745c5c3_Err != nil {
748 return templ_7745c5c3_Err790 return templ_7745c5c3_Err
749 }791 }
750- var templ_7745c5c3_Var39 string792+ var templ_7745c5c3_Var41 string
751- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)793+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
752 if templ_7745c5c3_Err != nil {794 if templ_7745c5c3_Err != nil {
753- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 165, Col: 39}795+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 178, Col: 39}
754 }796 }
755- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))797+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
756 if templ_7745c5c3_Err != nil {798 if templ_7745c5c3_Err != nil {
757 return templ_7745c5c3_Err799 return templ_7745c5c3_Err
758 }800 }
759- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</kbd></a></li>")801+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</kbd></a></li>")
760 if templ_7745c5c3_Err != nil {802 if templ_7745c5c3_Err != nil {
761 return templ_7745c5c3_Err803 return templ_7745c5c3_Err
762 }804 }
763 }805 }
764- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</ul></nav>")806+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</ul></nav>")
765 if templ_7745c5c3_Err != nil {807 if templ_7745c5c3_Err != nil {
766 return templ_7745c5c3_Err808 return templ_7745c5c3_Err
767 }809 }
768 }810 }
769- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</header>")811+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</header>")
770 if templ_7745c5c3_Err != nil {812 if templ_7745c5c3_Err != nil {
771 return templ_7745c5c3_Err813 return templ_7745c5c3_Err
772 }814 }
@@ -792,38 +834,38 @@ func Footer(l Layout) templ.Component {
792 }()834 }()
793 }835 }
794 ctx = templ.InitializeContext(ctx)836 ctx = templ.InitializeContext(ctx)
795- templ_7745c5c3_Var40 := templ.GetChildren(ctx)837+ templ_7745c5c3_Var42 := templ.GetChildren(ctx)
796- if templ_7745c5c3_Var40 == nil {838+ if templ_7745c5c3_Var42 == nil {
797- templ_7745c5c3_Var40 = templ.NopComponent839+ templ_7745c5c3_Var42 = templ.NopComponent
798 }840 }
799 ctx = templ.ClearChildren(ctx)841 ctx = templ.ClearChildren(ctx)
800- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<footer class=\"foot\"><span>ermite ")842+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<footer class=\"foot\"><span>ermite ")
801 if templ_7745c5c3_Err != nil {843 if templ_7745c5c3_Err != nil {
802 return templ_7745c5c3_Err844 return templ_7745c5c3_Err
803 }845 }
804- var templ_7745c5c3_Var41 string846+ var templ_7745c5c3_Var43 string
805- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)847+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
806 if templ_7745c5c3_Err != nil {848 if templ_7745c5c3_Err != nil {
807- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 26}849+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 192, Col: 26}
808 }850 }
809- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))851+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
810 if templ_7745c5c3_Err != nil {852 if templ_7745c5c3_Err != nil {
811 return templ_7745c5c3_Err853 return templ_7745c5c3_Err
812 }854 }
813- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</span> <a href=\"")855+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</span> <a href=\"")
814 if templ_7745c5c3_Err != nil {856 if templ_7745c5c3_Err != nil {
815 return templ_7745c5c3_Err857 return templ_7745c5c3_Err
816 }858 }
817- var templ_7745c5c3_Var42 templ.SafeURL859+ var templ_7745c5c3_Var44 templ.SafeURL
818- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))860+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
819 if templ_7745c5c3_Err != nil {861 if templ_7745c5c3_Err != nil {
820- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 180, Col: 38}862+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 193, Col: 38}
821 }863 }
822- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))864+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
823 if templ_7745c5c3_Err != nil {865 if templ_7745c5c3_Err != nil {
824 return templ_7745c5c3_Err866 return templ_7745c5c3_Err
825 }867 }
826- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")868+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
827 if templ_7745c5c3_Err != nil {869 if templ_7745c5c3_Err != nil {
828 return templ_7745c5c3_Err870 return templ_7745c5c3_Err
829 }871 }
@@ -851,12 +893,12 @@ func Hint(l Layout) templ.Component {
851 }()893 }()
852 }894 }
853 ctx = templ.InitializeContext(ctx)895 ctx = templ.InitializeContext(ctx)
854- templ_7745c5c3_Var43 := templ.GetChildren(ctx)896+ templ_7745c5c3_Var45 := templ.GetChildren(ctx)
855- if templ_7745c5c3_Var43 == nil {897+ if templ_7745c5c3_Var45 == nil {
856- templ_7745c5c3_Var43 = templ.NopComponent898+ templ_7745c5c3_Var45 = templ.NopComponent
857 }899 }
858 ctx = templ.ClearChildren(ctx)900 ctx = templ.ClearChildren(ctx)
859- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")901+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
860 if templ_7745c5c3_Err != nil {902 if templ_7745c5c3_Err != nil {
861 return templ_7745c5c3_Err903 return templ_7745c5c3_Err
862 }904 }
@@ -864,7 +906,7 @@ func Hint(l Layout) templ.Component {
864 if templ_7745c5c3_Err != nil {906 if templ_7745c5c3_Err != nil {
865 return templ_7745c5c3_Err907 return templ_7745c5c3_Err
866 }908 }
867- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</div>")909+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</div>")
868 if templ_7745c5c3_Err != nil {910 if templ_7745c5c3_Err != nil {
869 return templ_7745c5c3_Err911 return templ_7745c5c3_Err
870 }912 }
M internal/adapters/web/static/css/20-shell.css +9 -0
@@ -29,6 +29,7 @@
2929 .i-cards { --i: url(icons/cards.svg); }
3030 .i-split { --i: url(icons/split.svg); }
3131 .i-compact { --i: url(icons/compact.svg); }
32+ .i-mailbox { --i: url(icons/mailbox.svg); }
3233
3334 .pill {
3435 display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1);
@@ -77,6 +78,11 @@
7778 }
7879 /* the hermit by his fire: a grey picture in its own rounded square */
7980 .brand-mark { width: 32px; height: 32px; flex: none; background: url(icons/brand.svg) center / contain no-repeat; }
81+ /* the inbox, right of the logo on the home page: its number in a darker badge */
82+ .inbox-btn {
83+ flex: none;
84+ & .count { margin-left: 0; color: var(--ink-hi); background: var(--bg-canvas); box-shadow: inset 0 0 0 1px var(--border); }
85+ }
8086 .crumbs {
8187 min-width: 0; flex: 1 1 auto; overflow: hidden;
8288 & ol { display: flex; align-items: center; min-width: 0; font-family: var(--mono); font-size: var(--fs); }
@@ -133,6 +139,9 @@
133139 align-content: start;
134140 }
135141 .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); }
142+ /* the wide mode (w, ermiteWide): every page takes the whole width of the
143+ screen; the rail goes to the right edge, the content takes the rest */
144+ html.wide :is(.work, .hdr-top, .tabs, .foot) { max-width: none; }
136145 .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }
137146 .work-lede { grid-column: 1 / -1; margin-bottom: calc(-1 * var(--s2)); }
138147 main > * + * { margin-top: var(--s4); }
@@ -29,6 +29,7 @@
29 .i-cards { --i: url(icons/cards.svg); }29 .i-cards { --i: url(icons/cards.svg); }
30 .i-split { --i: url(icons/split.svg); }30 .i-split { --i: url(icons/split.svg); }
31 .i-compact { --i: url(icons/compact.svg); }31 .i-compact { --i: url(icons/compact.svg); }
32+ .i-mailbox { --i: url(icons/mailbox.svg); }
32 33
33 .pill {34 .pill {
34 display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1);35 display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1);
@@ -77,6 +78,11 @@
77 }78 }
78 /* the hermit by his fire: a grey picture in its own rounded square */79 /* the hermit by his fire: a grey picture in its own rounded square */
79 .brand-mark { width: 32px; height: 32px; flex: none; background: url(icons/brand.svg) center / contain no-repeat; }80 .brand-mark { width: 32px; height: 32px; flex: none; background: url(icons/brand.svg) center / contain no-repeat; }
81+ /* the inbox, right of the logo on the home page: its number in a darker badge */
82+ .inbox-btn {
83+ flex: none;
84+ & .count { margin-left: 0; color: var(--ink-hi); background: var(--bg-canvas); box-shadow: inset 0 0 0 1px var(--border); }
85+ }
80 .crumbs {86 .crumbs {
81 min-width: 0; flex: 1 1 auto; overflow: hidden;87 min-width: 0; flex: 1 1 auto; overflow: hidden;
82 & ol { display: flex; align-items: center; min-width: 0; font-family: var(--mono); font-size: var(--fs); }88 & ol { display: flex; align-items: center; min-width: 0; font-family: var(--mono); font-size: var(--fs); }
@@ -133,6 +139,9 @@
133 align-content: start;139 align-content: start;
134 }140 }
135 .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); }141 .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); }
142+ /* the wide mode (w, ermiteWide): every page takes the whole width of the
143+ screen; the rail goes to the right edge, the content takes the rest */
144+ html.wide :is(.work, .hdr-top, .tabs, .foot) { max-width: none; }
136 .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }145 .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }
137 .work-lede { grid-column: 1 / -1; margin-bottom: calc(-1 * var(--s2)); }146 .work-lede { grid-column: 1 / -1; margin-bottom: calc(-1 * var(--s2)); }
138 main > * + * { margin-top: var(--s4); }147 main > * + * { margin-top: var(--s4); }
M internal/adapters/web/static/css/50-code.css +7 -4
@@ -79,13 +79,16 @@
7979 & .ln { width: calc(var(--lnw) + 2ch); min-width: calc(var(--lnw) + 2ch); padding: 0 1ch; text-align: right; color: var(--ink-mute); user-select: none; }
8080 & .m { width: 2ch; text-align: center; user-select: none; color: var(--ink-dim); }
8181 & .tx { white-space: pre; }
82- & .hunk td { color: var(--ink-mute); background: var(--paper-alt); padding: 0 1ch; }
82+ /* the hunk headers and the hidden lines stay quiet: a faint band between
83+ the code, so that the eye keeps reading */
84+ & .hunk td, & .gap td { background: color-mix(in srgb, var(--paper-alt) 35%, var(--paper)); }
85+ & .hunk td { color: var(--ink-mute); padding: 0 1ch; }
8386 /* a run of hidden lines: the whole row loads them */
84- & .gap td { padding: 0; background: var(--paper-alt); }
87+ & .gap td { padding: 0; }
8588 & .gap button {
8689 display: block; width: 100%; padding: 2px 1ch; text-align: left; cursor: pointer;
87- font: inherit; color: var(--ink-dim); background: none; border: 0;
88- border-block: 1px dashed var(--border);
90+ font: inherit; color: var(--ink-mute); background: none; border: 0;
91+ border-block: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
8992 }
9093 & .gap button:hover, & .gap button:focus-visible { color: var(--ink-hi); background: var(--bg-strong); }
9194 & .gap button:disabled { cursor: progress; opacity: .6; }
@@ -79,13 +79,16 @@
79 & .ln { width: calc(var(--lnw) + 2ch); min-width: calc(var(--lnw) + 2ch); padding: 0 1ch; text-align: right; color: var(--ink-mute); user-select: none; }79 & .ln { width: calc(var(--lnw) + 2ch); min-width: calc(var(--lnw) + 2ch); padding: 0 1ch; text-align: right; color: var(--ink-mute); user-select: none; }
80 & .m { width: 2ch; text-align: center; user-select: none; color: var(--ink-dim); }80 & .m { width: 2ch; text-align: center; user-select: none; color: var(--ink-dim); }
81 & .tx { white-space: pre; }81 & .tx { white-space: pre; }
82- & .hunk td { color: var(--ink-mute); background: var(--paper-alt); padding: 0 1ch; }82+ /* the hunk headers and the hidden lines stay quiet: a faint band between
83+ the code, so that the eye keeps reading */
84+ & .hunk td, & .gap td { background: color-mix(in srgb, var(--paper-alt) 35%, var(--paper)); }
85+ & .hunk td { color: var(--ink-mute); padding: 0 1ch; }
83 /* a run of hidden lines: the whole row loads them */86 /* a run of hidden lines: the whole row loads them */
84- & .gap td { padding: 0; background: var(--paper-alt); }87+ & .gap td { padding: 0; }
85 & .gap button {88 & .gap button {
86 display: block; width: 100%; padding: 2px 1ch; text-align: left; cursor: pointer;89 display: block; width: 100%; padding: 2px 1ch; text-align: left; cursor: pointer;
87- font: inherit; color: var(--ink-dim); background: none; border: 0;90+ font: inherit; color: var(--ink-mute); background: none; border: 0;
88- border-block: 1px dashed var(--border);91+ border-block: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
89 }92 }
90 & .gap button:hover, & .gap button:focus-visible { color: var(--ink-hi); background: var(--bg-strong); }93 & .gap button:hover, & .gap button:focus-visible { color: var(--ink-hi); background: var(--bg-strong); }
91 & .gap button:disabled { cursor: progress; opacity: .6; }94 & .gap button:disabled { cursor: progress; opacity: .6; }
M internal/adapters/web/static/css/55-diff.css +25 -0
@@ -9,4 +9,29 @@
99
1010 @layer components {
1111 .sel { outline: 1px solid var(--ink-dim); outline-offset: -1px; }
12+
13+ /* a range of selected diff lines (shift + click) is one block: one border
14+ around the run, none between its lines. Each edge is an inset shadow of
15+ the cells; the top one only on the first line of the run, the bottom one
16+ only on the last. In split, each side is a block of its own. */
17+ .dt {
18+ --sel-edge: var(--ink-dim);
19+ & .sel { outline: none; }
20+ & tr.sel > td, & td.sel {
21+ box-shadow: var(--sel-l, 0 0 transparent), var(--sel-r, 0 0 transparent), var(--sel-t, 0 0 transparent), var(--sel-b, 0 0 transparent);
22+ --sel-t: inset 0 1px var(--sel-edge); --sel-b: inset 0 -1px var(--sel-edge);
23+ }
24+ /* unified: the row is selected */
25+ & tr.sel > td:first-child { --sel-l: inset 1px 0 var(--sel-edge); }
26+ & tr.sel > td:last-child { --sel-r: inset -1px 0 var(--sel-edge); }
27+ & tr.sel + tr.sel > td { --sel-t: 0 0 transparent; }
28+ & tr.sel:has(+ tr.sel) > td { --sel-b: 0 0 transparent; }
29+ /* split: the cells of one side (number, mark, text) are selected */
30+ & td.sel.ln { --sel-l: inset 1px 0 var(--sel-edge); }
31+ & td.sel.tx { --sel-r: inset -1px 0 var(--sel-edge); }
32+ & tr:has(> :nth-child(3).sel) + tr > :nth-child(-n + 3).sel,
33+ & tr:has(> :nth-child(6).sel) + tr > :nth-child(n + 4).sel { --sel-t: 0 0 transparent; }
34+ & tr:has(> :nth-child(3).sel):has(+ tr > :nth-child(3).sel) > :nth-child(-n + 3).sel,
35+ & tr:has(> :nth-child(6).sel):has(+ tr > :nth-child(6).sel) > :nth-child(n + 4).sel { --sel-b: 0 0 transparent; }
36+ }
1237 }
@@ -9,4 +9,29 @@
9 9
10 @layer components {10 @layer components {
11 .sel { outline: 1px solid var(--ink-dim); outline-offset: -1px; }11 .sel { outline: 1px solid var(--ink-dim); outline-offset: -1px; }
12+
13+ /* a range of selected diff lines (shift + click) is one block: one border
14+ around the run, none between its lines. Each edge is an inset shadow of
15+ the cells; the top one only on the first line of the run, the bottom one
16+ only on the last. In split, each side is a block of its own. */
17+ .dt {
18+ --sel-edge: var(--ink-dim);
19+ & .sel { outline: none; }
20+ & tr.sel > td, & td.sel {
21+ box-shadow: var(--sel-l, 0 0 transparent), var(--sel-r, 0 0 transparent), var(--sel-t, 0 0 transparent), var(--sel-b, 0 0 transparent);
22+ --sel-t: inset 0 1px var(--sel-edge); --sel-b: inset 0 -1px var(--sel-edge);
23+ }
24+ /* unified: the row is selected */
25+ & tr.sel > td:first-child { --sel-l: inset 1px 0 var(--sel-edge); }
26+ & tr.sel > td:last-child { --sel-r: inset -1px 0 var(--sel-edge); }
27+ & tr.sel + tr.sel > td { --sel-t: 0 0 transparent; }
28+ & tr.sel:has(+ tr.sel) > td { --sel-b: 0 0 transparent; }
29+ /* split: the cells of one side (number, mark, text) are selected */
30+ & td.sel.ln { --sel-l: inset 1px 0 var(--sel-edge); }
31+ & td.sel.tx { --sel-r: inset -1px 0 var(--sel-edge); }
32+ & tr:has(> :nth-child(3).sel) + tr > :nth-child(-n + 3).sel,
33+ & tr:has(> :nth-child(6).sel) + tr > :nth-child(n + 4).sel { --sel-t: 0 0 transparent; }
34+ & tr:has(> :nth-child(3).sel):has(+ tr > :nth-child(3).sel) > :nth-child(-n + 3).sel,
35+ & tr:has(> :nth-child(6).sel):has(+ tr > :nth-child(6).sel) > :nth-child(n + 4).sel { --sel-b: 0 0 transparent; }
36+ }
12 }37 }
M internal/adapters/web/static/css/60-mr.css +15 -1
@@ -1,6 +1,12 @@
11 @layer components {
22 /* merge requests: the review page. Hue only where it carries a verdict. */
33 .mr-h .show-kv { margin-top: 0; }
4+ /* one card: the header (title, facts) a shade lighter, the description below */
5+ .mr-card {
6+ background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden;
7+ & > .mr-h { background: var(--bg-raised); border: 0; border-radius: 0; padding-bottom: var(--s3); }
8+ & > .mr-desc.prose { border: 0; border-top: 1px solid var(--border); border-radius: 0; }
9+ }
410
511 .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); }
612 .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); }
@@ -49,6 +55,15 @@
4955 }
5056 & .kv { margin: 0; }
5157 }
58+ /* the view menu of a diff: the layout and the fold, one row each */
59+ .vmenu {
60+ & > summary::after { content: ""; width: 0; height: 0; margin-left: 2px; border: 4px solid transparent; border-top-color: currentColor; transform: translateY(2px); }
61+ & .menu-pop { left: 0; right: auto; width: 220px; gap: var(--s1); padding: var(--s2); }
62+ & .menu-pop .btn { width: 100%; justify-content: flex-start; border-color: transparent; background: none; }
63+ & .menu-pop .btn:hover { background: var(--bg-raised); }
64+ & .menu-pop .btn[aria-current="true"] { background: var(--bg-raised); color: var(--ink-hi); }
65+ & .menu-pop .btn kbd { margin-left: auto; }
66+ }
5267 .mmenu {
5368 & .d-modes { display: grid; gap: var(--s1); border: 0; padding: 0; margin: 0; }
5469 & .d-modes label, & .d-check {
@@ -207,7 +222,6 @@
207222 & .inbox-counts strong { color: var(--ink-hi); font-family: var(--mono); }
208223 & .since { align-self: flex-start; max-width: 100%; }
209224 }
210- .inbox-btn { gap: var(--s2); }
211225
212226 @media (max-width: 700px) {
213227 .list.mrs .c-who { display: none; }
@@ -1,6 +1,12 @@
1 @layer components {1 @layer components {
2 /* merge requests: the review page. Hue only where it carries a verdict. */2 /* merge requests: the review page. Hue only where it carries a verdict. */
3 .mr-h .show-kv { margin-top: 0; }3 .mr-h .show-kv { margin-top: 0; }
4+ /* one card: the header (title, facts) a shade lighter, the description below */
5+ .mr-card {
6+ background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden;
7+ & > .mr-h { background: var(--bg-raised); border: 0; border-radius: 0; padding-bottom: var(--s3); }
8+ & > .mr-desc.prose { border: 0; border-top: 1px solid var(--border); border-radius: 0; }
9+ }
4 10
5 .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); }11 .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); }
6 .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); }12 .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); }
@@ -49,6 +55,15 @@
49 }55 }
50 & .kv { margin: 0; }56 & .kv { margin: 0; }
51 }57 }
58+ /* the view menu of a diff: the layout and the fold, one row each */
59+ .vmenu {
60+ & > summary::after { content: ""; width: 0; height: 0; margin-left: 2px; border: 4px solid transparent; border-top-color: currentColor; transform: translateY(2px); }
61+ & .menu-pop { left: 0; right: auto; width: 220px; gap: var(--s1); padding: var(--s2); }
62+ & .menu-pop .btn { width: 100%; justify-content: flex-start; border-color: transparent; background: none; }
63+ & .menu-pop .btn:hover { background: var(--bg-raised); }
64+ & .menu-pop .btn[aria-current="true"] { background: var(--bg-raised); color: var(--ink-hi); }
65+ & .menu-pop .btn kbd { margin-left: auto; }
66+ }
52 .mmenu {67 .mmenu {
53 & .d-modes { display: grid; gap: var(--s1); border: 0; padding: 0; margin: 0; }68 & .d-modes { display: grid; gap: var(--s1); border: 0; padding: 0; margin: 0; }
54 & .d-modes label, & .d-check {69 & .d-modes label, & .d-check {
@@ -207,7 +222,6 @@
207 & .inbox-counts strong { color: var(--ink-hi); font-family: var(--mono); }222 & .inbox-counts strong { color: var(--ink-hi); font-family: var(--mono); }
208 & .since { align-self: flex-start; max-width: 100%; }223 & .since { align-self: flex-start; max-width: 100%; }
209 }224 }
210- .inbox-btn { gap: var(--s2); }
211 225
212 @media (max-width: 700px) {226 @media (max-width: 700px) {
213 .list.mrs .c-who { display: none; }227 .list.mrs .c-who { display: none; }
A internal/adapters/web/static/icons/mailbox.svg +1 -0
@@ -0,0 +1,1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9.5C2 7 4 5 6.5 5H18c2.2 0 4 1.8 4 4v8Z"/><polyline points="15,9 18,9 18,11"/><path d="M6.5 5C9 5 11 7 11 9.5V17a2 2 0 0 1-2 2"/><line x1="6" x2="7" y1="10" y2="10"/></svg>
@@ -0,0 +1,1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9.5C2 7 4 5 6.5 5H18c2.2 0 4 1.8 4 4v8Z"/><polyline points="15,9 18,9 18,11"/><path d="M6.5 5C9 5 11 7 11 9.5V17a2 2 0 0 1-2 2"/><line x1="6" x2="7" y1="10" y2="10"/></svg>
M internal/adapters/web/static/js/ermite.js +70 -3
@@ -14,6 +14,7 @@
1414 // 7. The service worker (ermiteInstall).
1515 // 8. Chip lists kept on one line, with a … chip (ermiteFit).
1616 // 9. The layout of the home page, remembered in the browser (ermiteLayout).
17+// 10. The wide mode: every page takes the whole width (ermiteWide).
1718 //
1819 // Preferences are kept by ermiteStore (store.js, loaded first).
1920 (() => {
@@ -400,6 +401,25 @@
400401 // file whose lines are all loaded has no gap left.
401402 window.ermiteGaps = (() => {
402403 const pending = new Map();
404+ // selectLoaded marks the loaded rows that fall in the range of lines the
405+ // URL selects (?on=new:21-149:a.go, a shift + click), as the server marks
406+ // the rows it sent: the range stays one block once its hidden lines show.
407+ // The loaded rows are context lines, the same on both sides.
408+ function selectLoaded(file, rows) {
409+ const m = /^(new|old):(?:(\d+)-)?(\d+):(.+)$/.exec(new URLSearchParams(location.search).get('on') || '');
410+ // data-file is the path comments name (the new path of a rename), as ?on=
411+ if (!m || file.dataset.file !== m[4]) return;
412+ const end = Number(m[3]);
413+ const start = m[2] ? Number(m[2]) : end;
414+ for (const r of rows) {
415+ const cells = [...r.querySelectorAll('td')];
416+ // unified: old, new, mark, text; split: old side, then new side
417+ const num = Number((cells.length === 4 ? cells[m[1] === 'old' ? 0 : 1] : cells[m[1] === 'old' ? 0 : 3])?.textContent.trim());
418+ if (!(num >= start && num <= end)) continue;
419+ if (cells.length === 4) r.classList.add('sel');
420+ else for (const c of cells) c.classList.add('sel');
421+ }
422+ }
403423 // load fills the gap the button is in, in both tables of its file.
404424 function load(btn) {
405425 const src = btn.dataset.src;
@@ -424,6 +444,7 @@
424444 if (!from || !at) continue;
425445 const rows = [...from.children].map((r) => r.cloneNode(true));
426446 n = Math.max(n, rows.filter((r) => !r.classList.contains('gap')).length);
447+ selectLoaded(file, rows.filter((r) => !r.classList.contains('gap')));
427448 at.replaceWith(...rows);
428449 }
429450 // The focus was on the button, which is gone: on the gap that is
@@ -553,9 +574,44 @@
553574 return { get: pref.get, set, apply, values };
554575 })();
555576
577+ // The wide mode (w, on every page): the work area takes the whole width of
578+ // the screen, the rail at the right edge and the content in the rest.
579+ // The choice is kept by ermiteStore ("wide": '1' when wide) and shown
580+ // as html.wide, which the styles follow. Another tab's change applies here
581+ // at once.
582+ window.ermiteWide = (() => {
583+ const pref = window.ermiteStore.pref('wide', { values: ['1', ''], fallback: '' });
584+ function apply() {
585+ const wide = pref.get() === '1';
586+ document.documentElement.classList.toggle?.('wide', wide);
587+ }
588+ function toggle() {
589+ pref.set(pref.get() === '1' ? '' : '1');
590+ apply();
591+ }
592+ pref.watch(apply);
593+ apply();
594+ return { toggle, apply };
595+ })();
596+
597+ // The menus of the bars (<details class="menu">: the diff view, the merge
598+ // menu): closing one hands the focus back to its button when it was inside,
599+ // so that a keyboard user keeps their place.
600+ function closeMenu(d) {
601+ const had = d.contains(document.activeElement);
602+ d.open = false;
603+ if (had) $('summary', d)?.focus();
604+ }
605+ // closeMenus closes every open menu (escape); true when one was open.
606+ function closeMenus() {
607+ const open = $$('details.menu[open]');
608+ for (const d of open) closeMenu(d);
609+ return open.length > 0;
610+ }
611+
556612 // 1. Keys.
557613 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',
558- s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
614+ s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
559615 // the merge request page (mr.js): the merge menu, mark read
560616 M: 'merge', R: 'read' };
561617 // "g t" is the key "t": the tabs have one event each.
@@ -571,7 +627,7 @@
571627 const mr = window.ermiteMR;
572628 if (e.key === 'Escape') {
573629 pending = 0;
574- if (mr?.closeMenus() || mr?.leaveFiles()) return;
630+ if (closeMenus() || mr?.leaveFiles()) return;
575631 if (typing) e.target.blur();
576632 return ermite.fire('esc');
577633 }
@@ -611,6 +667,13 @@
611667 if (!el || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
612668 e.preventDefault();
613669 ermite.fire(el.dataset.fire);
670+ // A choice in a menu closes it; the focus goes back to its button.
671+ const menu = el.closest('details.menu');
672+ if (menu) closeMenu(menu);
673+ });
674+ // A click outside an open menu closes it.
675+ addEventListener('click', (e) => {
676+ for (const d of $$('details.menu[open]')) if (!d.contains(e.target)) d.open = false;
614677 });
615678 for (const a of $$('a[data-fire="refs"]')) {
616679 a.setAttribute('aria-haspopup', 'dialog');
@@ -773,7 +836,8 @@
773836 // internal/adapters/web/events.go). A fragment is a top-level element with an
774837 // id: it replaces the element of the page that has the same id. A fragment
775838 // marked data-gone removes it; a row of a repository the home page does not
776-// list yet is added at the top of the list. A slot (data-slot: the place of a
839+// list yet is added at the top of the list. Activity updates also move existing
840+// home rows to the top when sorted by newest activity; CI updates stay put. A slot (data-slot: the place of a
777841 // banner, a persistent status region) keeps its element and only gets the
778842 // children of the fragment, so that screen readers announce the change. A gap,
779843 // which says the page may have missed events or that what it shows changed,
@@ -872,6 +936,9 @@ window.ermiteLive = (() => {
872936 }
873937 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');
874938 old.replaceWith(el);
939+ const rows = document.getElementById('repo-rows');
940+ if (rows && el.parentElement === rows && rows.dataset.sort === 'activity' &&
941+ rows.dataset.desc === 'true' && el.hasAttribute('data-activity')) rows.prepend(el);
875942 window.htmx?.process(el);
876943 fitIn(el);
877944 if (had) refocus(el, had);
@@ -14,6 +14,7 @@
14 // 7. The service worker (ermiteInstall).14 // 7. The service worker (ermiteInstall).
15 // 8. Chip lists kept on one line, with a … chip (ermiteFit).15 // 8. Chip lists kept on one line, with a … chip (ermiteFit).
16 // 9. The layout of the home page, remembered in the browser (ermiteLayout).16 // 9. The layout of the home page, remembered in the browser (ermiteLayout).
17+// 10. The wide mode: every page takes the whole width (ermiteWide).
17 //18 //
18 // Preferences are kept by ermiteStore (store.js, loaded first).19 // Preferences are kept by ermiteStore (store.js, loaded first).
19 (() => {20 (() => {
@@ -400,6 +401,25 @@
400 // file whose lines are all loaded has no gap left.401 // file whose lines are all loaded has no gap left.
401 window.ermiteGaps = (() => {402 window.ermiteGaps = (() => {
402 const pending = new Map();403 const pending = new Map();
404+ // selectLoaded marks the loaded rows that fall in the range of lines the
405+ // URL selects (?on=new:21-149:a.go, a shift + click), as the server marks
406+ // the rows it sent: the range stays one block once its hidden lines show.
407+ // The loaded rows are context lines, the same on both sides.
408+ function selectLoaded(file, rows) {
409+ const m = /^(new|old):(?:(\d+)-)?(\d+):(.+)$/.exec(new URLSearchParams(location.search).get('on') || '');
410+ // data-file is the path comments name (the new path of a rename), as ?on=
411+ if (!m || file.dataset.file !== m[4]) return;
412+ const end = Number(m[3]);
413+ const start = m[2] ? Number(m[2]) : end;
414+ for (const r of rows) {
415+ const cells = [...r.querySelectorAll('td')];
416+ // unified: old, new, mark, text; split: old side, then new side
417+ const num = Number((cells.length === 4 ? cells[m[1] === 'old' ? 0 : 1] : cells[m[1] === 'old' ? 0 : 3])?.textContent.trim());
418+ if (!(num >= start && num <= end)) continue;
419+ if (cells.length === 4) r.classList.add('sel');
420+ else for (const c of cells) c.classList.add('sel');
421+ }
422+ }
403 // load fills the gap the button is in, in both tables of its file.423 // load fills the gap the button is in, in both tables of its file.
404 function load(btn) {424 function load(btn) {
405 const src = btn.dataset.src;425 const src = btn.dataset.src;
@@ -424,6 +444,7 @@
424 if (!from || !at) continue;444 if (!from || !at) continue;
425 const rows = [...from.children].map((r) => r.cloneNode(true));445 const rows = [...from.children].map((r) => r.cloneNode(true));
426 n = Math.max(n, rows.filter((r) => !r.classList.contains('gap')).length);446 n = Math.max(n, rows.filter((r) => !r.classList.contains('gap')).length);
447+ selectLoaded(file, rows.filter((r) => !r.classList.contains('gap')));
427 at.replaceWith(...rows);448 at.replaceWith(...rows);
428 }449 }
429 // The focus was on the button, which is gone: on the gap that is450 // The focus was on the button, which is gone: on the gap that is
@@ -553,9 +574,44 @@
553 return { get: pref.get, set, apply, values };574 return { get: pref.get, set, apply, values };
554 })();575 })();
555 576
577+ // The wide mode (w, on every page): the work area takes the whole width of
578+ // the screen, the rail at the right edge and the content in the rest.
579+ // The choice is kept by ermiteStore ("wide": '1' when wide) and shown
580+ // as html.wide, which the styles follow. Another tab's change applies here
581+ // at once.
582+ window.ermiteWide = (() => {
583+ const pref = window.ermiteStore.pref('wide', { values: ['1', ''], fallback: '' });
584+ function apply() {
585+ const wide = pref.get() === '1';
586+ document.documentElement.classList.toggle?.('wide', wide);
587+ }
588+ function toggle() {
589+ pref.set(pref.get() === '1' ? '' : '1');
590+ apply();
591+ }
592+ pref.watch(apply);
593+ apply();
594+ return { toggle, apply };
595+ })();
596+
597+ // The menus of the bars (<details class="menu">: the diff view, the merge
598+ // menu): closing one hands the focus back to its button when it was inside,
599+ // so that a keyboard user keeps their place.
600+ function closeMenu(d) {
601+ const had = d.contains(document.activeElement);
602+ d.open = false;
603+ if (had) $('summary', d)?.focus();
604+ }
605+ // closeMenus closes every open menu (escape); true when one was open.
606+ function closeMenus() {
607+ const open = $$('details.menu[open]');
608+ for (const d of open) closeMenu(d);
609+ return open.length > 0;
610+ }
611+
556 // 1. Keys.612 // 1. Keys.
557 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',613 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',
558- s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',614+ s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
559 // the merge request page (mr.js): the merge menu, mark read615 // the merge request page (mr.js): the merge menu, mark read
560 M: 'merge', R: 'read' };616 M: 'merge', R: 'read' };
561 // "g t" is the key "t": the tabs have one event each.617 // "g t" is the key "t": the tabs have one event each.
@@ -571,7 +627,7 @@
571 const mr = window.ermiteMR;627 const mr = window.ermiteMR;
572 if (e.key === 'Escape') {628 if (e.key === 'Escape') {
573 pending = 0;629 pending = 0;
574- if (mr?.closeMenus() || mr?.leaveFiles()) return;630+ if (closeMenus() || mr?.leaveFiles()) return;
575 if (typing) e.target.blur();631 if (typing) e.target.blur();
576 return ermite.fire('esc');632 return ermite.fire('esc');
577 }633 }
@@ -611,6 +667,13 @@
611 if (!el || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;667 if (!el || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
612 e.preventDefault();668 e.preventDefault();
613 ermite.fire(el.dataset.fire);669 ermite.fire(el.dataset.fire);
670+ // A choice in a menu closes it; the focus goes back to its button.
671+ const menu = el.closest('details.menu');
672+ if (menu) closeMenu(menu);
673+ });
674+ // A click outside an open menu closes it.
675+ addEventListener('click', (e) => {
676+ for (const d of $$('details.menu[open]')) if (!d.contains(e.target)) d.open = false;
614 });677 });
615 for (const a of $$('a[data-fire="refs"]')) {678 for (const a of $$('a[data-fire="refs"]')) {
616 a.setAttribute('aria-haspopup', 'dialog');679 a.setAttribute('aria-haspopup', 'dialog');
@@ -773,7 +836,8 @@
773 // internal/adapters/web/events.go). A fragment is a top-level element with an836 // internal/adapters/web/events.go). A fragment is a top-level element with an
774 // id: it replaces the element of the page that has the same id. A fragment837 // id: it replaces the element of the page that has the same id. A fragment
775 // marked data-gone removes it; a row of a repository the home page does not838 // marked data-gone removes it; a row of a repository the home page does not
776-// list yet is added at the top of the list. A slot (data-slot: the place of a839+// list yet is added at the top of the list. Activity updates also move existing
840+// home rows to the top when sorted by newest activity; CI updates stay put. A slot (data-slot: the place of a
777 // banner, a persistent status region) keeps its element and only gets the841 // banner, a persistent status region) keeps its element and only gets the
778 // children of the fragment, so that screen readers announce the change. A gap,842 // children of the fragment, so that screen readers announce the change. A gap,
779 // which says the page may have missed events or that what it shows changed,843 // which says the page may have missed events or that what it shows changed,
@@ -872,6 +936,9 @@ window.ermiteLive = (() => {
872 }936 }
873 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');937 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');
874 old.replaceWith(el);938 old.replaceWith(el);
939+ const rows = document.getElementById('repo-rows');
940+ if (rows && el.parentElement === rows && rows.dataset.sort === 'activity' &&
941+ rows.dataset.desc === 'true' && el.hasAttribute('data-activity')) rows.prepend(el);
875 window.htmx?.process(el);942 window.htmx?.process(el);
876 fitIn(el);943 fitIn(el);
877 if (had) refocus(el, had);944 if (had) refocus(el, had);
M internal/adapters/web/static/js/mr.js +1 -11
@@ -196,12 +196,6 @@ window.ermiteMR = (() => {
196196 $('[data-default]', m)?.focus();
197197 return true;
198198 }
199- function closeMenus() {
200- const open = $$('details.menu[open]');
201- for (const d of open) d.open = false;
202- if (open.length) $('summary', open[0])?.focus();
203- return open.length > 0;
204- }
205199 // menuKey handles a key while the merge menu is open; true when it used it.
206200 function menuKey(e) {
207201 const m = menu();
@@ -311,13 +305,9 @@ window.ermiteMR = (() => {
311305 try { sessionStorage.setItem('mr-last-line', lastLine); } catch { /* storage optional */ }
312306 if (next) { e.preventDefault(); location.assign(next); }
313307 });
314- // A click outside an open menu closes it.
315- addEventListener('click', (e) => {
316- for (const d of $$('details.menu[open]')) if (!d.contains(e.target)) d.open = false;
317- });
318308 }
319309
320- return { core, start, load, reveal, openMerge, closeMenus, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile };
310+ return { core, start, load, reveal, openMerge, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile };
321311 })();
322312
323313 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => window.ermiteMR.start());
@@ -196,12 +196,6 @@ window.ermiteMR = (() => {
196 $('[data-default]', m)?.focus();196 $('[data-default]', m)?.focus();
197 return true;197 return true;
198 }198 }
199- function closeMenus() {
200- const open = $$('details.menu[open]');
201- for (const d of open) d.open = false;
202- if (open.length) $('summary', open[0])?.focus();
203- return open.length > 0;
204- }
205 // menuKey handles a key while the merge menu is open; true when it used it.199 // menuKey handles a key while the merge menu is open; true when it used it.
206 function menuKey(e) {200 function menuKey(e) {
207 const m = menu();201 const m = menu();
@@ -311,13 +305,9 @@ window.ermiteMR = (() => {
311 try { sessionStorage.setItem('mr-last-line', lastLine); } catch { /* storage optional */ }305 try { sessionStorage.setItem('mr-last-line', lastLine); } catch { /* storage optional */ }
312 if (next) { e.preventDefault(); location.assign(next); }306 if (next) { e.preventDefault(); location.assign(next); }
313 });307 });
314- // A click outside an open menu closes it.
315- addEventListener('click', (e) => {
316- for (const d of $$('details.menu[open]')) if (!d.contains(e.target)) d.open = false;
317- });
318 }308 }
319 309
320- return { core, start, load, reveal, openMerge, closeMenus, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile };310+ return { core, start, load, reveal, openMerge, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile };
321 })();311 })();
322 312
323 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => window.ermiteMR.start());313 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => window.ermiteMR.start());
M internal/adapters/web/static/js/store.js +5 -0
@@ -74,3 +74,8 @@ window.ermiteStore = (() => {
7474
7575 return { get, set, remove, watch, pref };
7676 })();
77+
78+// The wide mode (ermiteWide in ermite.js) is set here, before the first paint:
79+// shell.templ loads this file without defer, so a wide page never draws narrow
80+// first.
81+if (window.ermiteStore.get('wide') === '1') document?.documentElement?.classList?.add('wide');
@@ -74,3 +74,8 @@ window.ermiteStore = (() => {
74 74
75 return { get, set, remove, watch, pref };75 return { get, set, remove, watch, pref };
76 })();76 })();
77+
78+// The wide mode (ermiteWide in ermite.js) is set here, before the first paint:
79+// shell.templ loads this file without defer, so a wide page never draws narrow
80+// first.
81+if (window.ermiteStore.get('wide') === '1') document?.documentElement?.classList?.add('wide');
M internal/adapters/web/view.go +6 -4
@@ -71,6 +71,9 @@ type Layout struct {
7171 MR bool
7272 // IssueForm loads the script of the public issue form (issue-form.js).
7373 IssueForm bool
74+ // Inbox is the button to the owner's inbox, next to the logo; set on the
75+ // home page only, for the signed-in owner.
76+ Inbox *InboxButton
7477 }
7578
7679 // Link is a plain label + href pair.
@@ -93,7 +96,7 @@ type HomeRow struct {
9396 Stats *HomeStats
9497 // Private marks a private repository; only set for a signed-in visitor.
9598 Private bool
96- // Fresh marks a row that a push just updated (live updates only).
99+ // Fresh marks a row that activity just updated (live updates only).
97100 Fresh bool
98101 // Commits are the newest of the default branch. NoCommits says there is
99102 // none (never pushed, or an empty branch); a failed read says nothing.
@@ -135,11 +138,10 @@ type SortLink struct {
135138
136139 // HomePage is the repository list.
137140 type HomePage struct {
141+ Sort string
142+ Desc bool
138143 Sorts []SortLink
139144 Rows []HomeRow
140- // Inbox is the button to the owner's inbox; nil unless the owner is signed
141- // in.
142- Inbox *InboxButton
143145 }
144146
145147 // InboxButton is the link to the inbox with the number of open items; Count is
@@ -71,6 +71,9 @@ type Layout struct {
71 MR bool71 MR bool
72 // IssueForm loads the script of the public issue form (issue-form.js).72 // IssueForm loads the script of the public issue form (issue-form.js).
73 IssueForm bool73 IssueForm bool
74+ // Inbox is the button to the owner's inbox, next to the logo; set on the
75+ // home page only, for the signed-in owner.
76+ Inbox *InboxButton
74 }77 }
75 78
76 // Link is a plain label + href pair.79 // Link is a plain label + href pair.
@@ -93,7 +96,7 @@ type HomeRow struct {
93 Stats *HomeStats96 Stats *HomeStats
94 // Private marks a private repository; only set for a signed-in visitor.97 // Private marks a private repository; only set for a signed-in visitor.
95 Private bool98 Private bool
96- // Fresh marks a row that a push just updated (live updates only).99+ // Fresh marks a row that activity just updated (live updates only).
97 Fresh bool100 Fresh bool
98 // Commits are the newest of the default branch. NoCommits says there is101 // Commits are the newest of the default branch. NoCommits says there is
99 // none (never pushed, or an empty branch); a failed read says nothing.102 // none (never pushed, or an empty branch); a failed read says nothing.
@@ -135,11 +138,10 @@ type SortLink struct {
135 138
136 // HomePage is the repository list.139 // HomePage is the repository list.
137 type HomePage struct {140 type HomePage struct {
141+ Sort string
142+ Desc bool
138 Sorts []SortLink143 Sorts []SortLink
139 Rows []HomeRow144 Rows []HomeRow
140- // Inbox is the button to the owner's inbox; nil unless the owner is signed
141- // in.
142- Inbox *InboxButton
143 }145 }
144 146
145 // InboxButton is the link to the inbox with the number of open items; Count is147 // InboxButton is the link to the inbox with the number of open items; Count is
M internal/app/testdata/web/blob-binary.html +8 -1
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -239,6 +239,12 @@
239239 </div>
240240 <div>
241241 <dt>
242+<kbd>w</kbd>
243+</dt>
244+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
245+</div>
246+<div>
247+<dt>
242248 <kbd>y</kbd>
243249 </dt>
244250 <dd>copy a permalink pinned to the commit</dd>
@@ -416,6 +422,7 @@
416422 k-split from:body -> ermite.layout('split');
417423 k-unified from:body -> ermite.layout('unified');
418424 k-collapse from:body -> ermite.collapse();
425+ k-wide from:body -> window.ermiteWide.toggle();
419426 k-expand from:body -> ermite.expandFile();
420427 k-expandAll from:body -> ermite.expandAll();
421428 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -239,6 +239,12 @@
239 </div>239 </div>
240 <div>240 <div>
241 <dt>241 <dt>
242+<kbd>w</kbd>
243+</dt>
244+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
245+</div>
246+<div>
247+<dt>
242 <kbd>y</kbd>248 <kbd>y</kbd>
243 </dt>249 </dt>
244 <dd>copy a permalink pinned to the commit</dd>250 <dd>copy a permalink pinned to the commit</dd>
@@ -416,6 +422,7 @@
416 k-split from:body -> ermite.layout('split');422 k-split from:body -> ermite.layout('split');
417 k-unified from:body -> ermite.layout('unified');423 k-unified from:body -> ermite.layout('unified');
418 k-collapse from:body -> ermite.collapse();424 k-collapse from:body -> ermite.collapse();
425+ k-wide from:body -> window.ermiteWide.toggle();
419 k-expand from:body -> ermite.expandFile();426 k-expand from:body -> ermite.expandFile();
420 k-expandAll from:body -> ermite.expandAll();427 k-expandAll from:body -> ermite.expandAll();
421 k-permalink from:body -> ermite.permalink();428 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/blob-code.html +8 -1
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -309,6 +309,12 @@
309309 </div>
310310 <div>
311311 <dt>
312+<kbd>w</kbd>
313+</dt>
314+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
315+</div>
316+<div>
317+<dt>
312318 <kbd>y</kbd>
313319 </dt>
314320 <dd>copy a permalink pinned to the commit</dd>
@@ -486,6 +492,7 @@
486492 k-split from:body -> ermite.layout('split');
487493 k-unified from:body -> ermite.layout('unified');
488494 k-collapse from:body -> ermite.collapse();
495+ k-wide from:body -> window.ermiteWide.toggle();
489496 k-expand from:body -> ermite.expandFile();
490497 k-expandAll from:body -> ermite.expandAll();
491498 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -309,6 +309,12 @@
309 </div>309 </div>
310 <div>310 <div>
311 <dt>311 <dt>
312+<kbd>w</kbd>
313+</dt>
314+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
315+</div>
316+<div>
317+<dt>
312 <kbd>y</kbd>318 <kbd>y</kbd>
313 </dt>319 </dt>
314 <dd>copy a permalink pinned to the commit</dd>320 <dd>copy a permalink pinned to the commit</dd>
@@ -486,6 +492,7 @@
486 k-split from:body -> ermite.layout('split');492 k-split from:body -> ermite.layout('split');
487 k-unified from:body -> ermite.layout('unified');493 k-unified from:body -> ermite.layout('unified');
488 k-collapse from:body -> ermite.collapse();494 k-collapse from:body -> ermite.collapse();
495+ k-wide from:body -> window.ermiteWide.toggle();
489 k-expand from:body -> ermite.expandFile();496 k-expand from:body -> ermite.expandFile();
490 k-expandAll from:body -> ermite.expandAll();497 k-expandAll from:body -> ermite.expandAll();
491 k-permalink from:body -> ermite.permalink();498 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/blob-empty.html +8 -1
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -239,6 +239,12 @@
239239 </div>
240240 <div>
241241 <dt>
242+<kbd>w</kbd>
243+</dt>
244+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
245+</div>
246+<div>
247+<dt>
242248 <kbd>y</kbd>
243249 </dt>
244250 <dd>copy a permalink pinned to the commit</dd>
@@ -416,6 +422,7 @@
416422 k-split from:body -> ermite.layout('split');
417423 k-unified from:body -> ermite.layout('unified');
418424 k-collapse from:body -> ermite.collapse();
425+ k-wide from:body -> window.ermiteWide.toggle();
419426 k-expand from:body -> ermite.expandFile();
420427 k-expandAll from:body -> ermite.expandAll();
421428 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -239,6 +239,12 @@
239 </div>239 </div>
240 <div>240 <div>
241 <dt>241 <dt>
242+<kbd>w</kbd>
243+</dt>
244+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
245+</div>
246+<div>
247+<dt>
242 <kbd>y</kbd>248 <kbd>y</kbd>
243 </dt>249 </dt>
244 <dd>copy a permalink pinned to the commit</dd>250 <dd>copy a permalink pinned to the commit</dd>
@@ -416,6 +422,7 @@
416 k-split from:body -> ermite.layout('split');422 k-split from:body -> ermite.layout('split');
417 k-unified from:body -> ermite.layout('unified');423 k-unified from:body -> ermite.layout('unified');
418 k-collapse from:body -> ermite.collapse();424 k-collapse from:body -> ermite.collapse();
425+ k-wide from:body -> window.ermiteWide.toggle();
419 k-expand from:body -> ermite.expandFile();426 k-expand from:body -> ermite.expandFile();
420 k-expandAll from:body -> ermite.expandAll();427 k-expandAll from:body -> ermite.expandAll();
421 k-permalink from:body -> ermite.permalink();428 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/blob-image.html +8 -1
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -241,6 +241,12 @@
241241 </div>
242242 <div>
243243 <dt>
244+<kbd>w</kbd>
245+</dt>
246+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
247+</div>
248+<div>
249+<dt>
244250 <kbd>y</kbd>
245251 </dt>
246252 <dd>copy a permalink pinned to the commit</dd>
@@ -418,6 +424,7 @@
418424 k-split from:body -> ermite.layout('split');
419425 k-unified from:body -> ermite.layout('unified');
420426 k-collapse from:body -> ermite.collapse();
427+ k-wide from:body -> window.ermiteWide.toggle();
421428 k-expand from:body -> ermite.expandFile();
422429 k-expandAll from:body -> ermite.expandAll();
423430 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -241,6 +241,12 @@
241 </div>241 </div>
242 <div>242 <div>
243 <dt>243 <dt>
244+<kbd>w</kbd>
245+</dt>
246+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
247+</div>
248+<div>
249+<dt>
244 <kbd>y</kbd>250 <kbd>y</kbd>
245 </dt>251 </dt>
246 <dd>copy a permalink pinned to the commit</dd>252 <dd>copy a permalink pinned to the commit</dd>
@@ -418,6 +424,7 @@
418 k-split from:body -> ermite.layout('split');424 k-split from:body -> ermite.layout('split');
419 k-unified from:body -> ermite.layout('unified');425 k-unified from:body -> ermite.layout('unified');
420 k-collapse from:body -> ermite.collapse();426 k-collapse from:body -> ermite.collapse();
427+ k-wide from:body -> window.ermiteWide.toggle();
421 k-expand from:body -> ermite.expandFile();428 k-expand from:body -> ermite.expandFile();
422 k-expandAll from:body -> ermite.expandAll();429 k-expandAll from:body -> ermite.expandAll();
423 k-permalink from:body -> ermite.permalink();430 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/blob-markdown.html +8 -1
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -306,6 +306,12 @@
306306 </div>
307307 <div>
308308 <dt>
309+<kbd>w</kbd>
310+</dt>
311+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
312+</div>
313+<div>
314+<dt>
309315 <kbd>y</kbd>
310316 </dt>
311317 <dd>copy a permalink pinned to the commit</dd>
@@ -483,6 +489,7 @@
483489 k-split from:body -> ermite.layout('split');
484490 k-unified from:body -> ermite.layout('unified');
485491 k-collapse from:body -> ermite.collapse();
492+ k-wide from:body -> window.ermiteWide.toggle();
486493 k-expand from:body -> ermite.expandFile();
487494 k-expandAll from:body -> ermite.expandAll();
488495 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -306,6 +306,12 @@
306 </div>306 </div>
307 <div>307 <div>
308 <dt>308 <dt>
309+<kbd>w</kbd>
310+</dt>
311+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
312+</div>
313+<div>
314+<dt>
309 <kbd>y</kbd>315 <kbd>y</kbd>
310 </dt>316 </dt>
311 <dd>copy a permalink pinned to the commit</dd>317 <dd>copy a permalink pinned to the commit</dd>
@@ -483,6 +489,7 @@
483 k-split from:body -> ermite.layout('split');489 k-split from:body -> ermite.layout('split');
484 k-unified from:body -> ermite.layout('unified');490 k-unified from:body -> ermite.layout('unified');
485 k-collapse from:body -> ermite.collapse();491 k-collapse from:body -> ermite.collapse();
492+ k-wide from:body -> window.ermiteWide.toggle();
486 k-expand from:body -> ermite.expandFile();493 k-expand from:body -> ermite.expandFile();
487 k-expandAll from:body -> ermite.expandAll();494 k-expandAll from:body -> ermite.expandAll();
488 k-permalink from:body -> ermite.permalink();495 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/blob-svg.html +8 -1
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -264,6 +264,12 @@
264264 </div>
265265 <div>
266266 <dt>
267+<kbd>w</kbd>
268+</dt>
269+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
270+</div>
271+<div>
272+<dt>
267273 <kbd>y</kbd>
268274 </dt>
269275 <dd>copy a permalink pinned to the commit</dd>
@@ -441,6 +447,7 @@
441447 k-split from:body -> ermite.layout('split');
442448 k-unified from:body -> ermite.layout('unified');
443449 k-collapse from:body -> ermite.collapse();
450+ k-wide from:body -> window.ermiteWide.toggle();
444451 k-expand from:body -> ermite.expandFile();
445452 k-expandAll from:body -> ermite.expandAll();
446453 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -264,6 +264,12 @@
264 </div>264 </div>
265 <div>265 <div>
266 <dt>266 <dt>
267+<kbd>w</kbd>
268+</dt>
269+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
270+</div>
271+<div>
272+<dt>
267 <kbd>y</kbd>273 <kbd>y</kbd>
268 </dt>274 </dt>
269 <dd>copy a permalink pinned to the commit</dd>275 <dd>copy a permalink pinned to the commit</dd>
@@ -441,6 +447,7 @@
441 k-split from:body -> ermite.layout('split');447 k-split from:body -> ermite.layout('split');
442 k-unified from:body -> ermite.layout('unified');448 k-unified from:body -> ermite.layout('unified');
443 k-collapse from:body -> ermite.collapse();449 k-collapse from:body -> ermite.collapse();
450+ k-wide from:body -> window.ermiteWide.toggle();
444 k-expand from:body -> ermite.expandFile();451 k-expand from:body -> ermite.expandFile();
445 k-expandAll from:body -> ermite.expandAll();452 k-expandAll from:body -> ermite.expandAll();
446 k-permalink from:body -> ermite.permalink();453 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/blob-text.html +8 -1
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -259,6 +259,12 @@
259259 </div>
260260 <div>
261261 <dt>
262+<kbd>w</kbd>
263+</dt>
264+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
265+</div>
266+<div>
267+<dt>
262268 <kbd>y</kbd>
263269 </dt>
264270 <dd>copy a permalink pinned to the commit</dd>
@@ -436,6 +442,7 @@
436442 k-split from:body -> ermite.layout('split');
437443 k-unified from:body -> ermite.layout('unified');
438444 k-collapse from:body -> ermite.collapse();
445+ k-wide from:body -> window.ermiteWide.toggle();
439446 k-expand from:body -> ermite.expandFile();
440447 k-expandAll from:body -> ermite.expandAll();
441448 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -259,6 +259,12 @@
259 </div>259 </div>
260 <div>260 <div>
261 <dt>261 <dt>
262+<kbd>w</kbd>
263+</dt>
264+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
265+</div>
266+<div>
267+<dt>
262 <kbd>y</kbd>268 <kbd>y</kbd>
263 </dt>269 </dt>
264 <dd>copy a permalink pinned to the commit</dd>270 <dd>copy a permalink pinned to the commit</dd>
@@ -436,6 +442,7 @@
436 k-split from:body -> ermite.layout('split');442 k-split from:body -> ermite.layout('split');
437 k-unified from:body -> ermite.layout('unified');443 k-unified from:body -> ermite.layout('unified');
438 k-collapse from:body -> ermite.collapse();444 k-collapse from:body -> ermite.collapse();
445+ k-wide from:body -> window.ermiteWide.toggle();
439 k-expand from:body -> ermite.expandFile();446 k-expand from:body -> ermite.expandFile();
440 k-expandAll from:body -> ermite.expandAll();447 k-expandAll from:body -> ermite.expandAll();
441 k-permalink from:body -> ermite.permalink();448 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/blob-too-large.html +8 -1
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -239,6 +239,12 @@
239239 </div>
240240 <div>
241241 <dt>
242+<kbd>w</kbd>
243+</dt>
244+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
245+</div>
246+<div>
247+<dt>
242248 <kbd>y</kbd>
243249 </dt>
244250 <dd>copy a permalink pinned to the commit</dd>
@@ -416,6 +422,7 @@
416422 k-split from:body -> ermite.layout('split');
417423 k-unified from:body -> ermite.layout('unified');
418424 k-collapse from:body -> ermite.collapse();
425+ k-wide from:body -> window.ermiteWide.toggle();
419426 k-expand from:body -> ermite.expandFile();
420427 k-expandAll from:body -> ermite.expandAll();
421428 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -239,6 +239,12 @@
239 </div>239 </div>
240 <div>240 <div>
241 <dt>241 <dt>
242+<kbd>w</kbd>
243+</dt>
244+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
245+</div>
246+<div>
247+<dt>
242 <kbd>y</kbd>248 <kbd>y</kbd>
243 </dt>249 </dt>
244 <dd>copy a permalink pinned to the commit</dd>250 <dd>copy a permalink pinned to the commit</dd>
@@ -416,6 +422,7 @@
416 k-split from:body -> ermite.layout('split');422 k-split from:body -> ermite.layout('split');
417 k-unified from:body -> ermite.layout('unified');423 k-unified from:body -> ermite.layout('unified');
418 k-collapse from:body -> ermite.collapse();424 k-collapse from:body -> ermite.collapse();
425+ k-wide from:body -> window.ermiteWide.toggle();
419 k-expand from:body -> ermite.expandFile();426 k-expand from:body -> ermite.expandFile();
420 k-expandAll from:body -> ermite.expandAll();427 k-expandAll from:body -> ermite.expandAll();
421 k-permalink from:body -> ermite.permalink();428 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/blob-trojan.html +8 -1
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -306,6 +306,12 @@
306306 </div>
307307 <div>
308308 <dt>
309+<kbd>w</kbd>
310+</dt>
311+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
312+</div>
313+<div>
314+<dt>
309315 <kbd>y</kbd>
310316 </dt>
311317 <dd>copy a permalink pinned to the commit</dd>
@@ -483,6 +489,7 @@
483489 k-split from:body -> ermite.layout('split');
484490 k-unified from:body -> ermite.layout('unified');
485491 k-collapse from:body -> ermite.collapse();
492+ k-wide from:body -> window.ermiteWide.toggle();
486493 k-expand from:body -> ermite.expandFile();
487494 k-expandAll from:body -> ermite.expandAll();
488495 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -306,6 +306,12 @@
306 </div>306 </div>
307 <div>307 <div>
308 <dt>308 <dt>
309+<kbd>w</kbd>
310+</dt>
311+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
312+</div>
313+<div>
314+<dt>
309 <kbd>y</kbd>315 <kbd>y</kbd>
310 </dt>316 </dt>
311 <dd>copy a permalink pinned to the commit</dd>317 <dd>copy a permalink pinned to the commit</dd>
@@ -483,6 +489,7 @@
483 k-split from:body -> ermite.layout('split');489 k-split from:body -> ermite.layout('split');
484 k-unified from:body -> ermite.layout('unified');490 k-unified from:body -> ermite.layout('unified');
485 k-collapse from:body -> ermite.collapse();491 k-collapse from:body -> ermite.collapse();
492+ k-wide from:body -> window.ermiteWide.toggle();
486 k-expand from:body -> ermite.expandFile();493 k-expand from:body -> ermite.expandFile();
487 k-expandAll from:body -> ermite.expandAll();494 k-expandAll from:body -> ermite.expandAll();
488 k-permalink from:body -> ermite.permalink();495 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/commit-big.html +16 -2
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -125,6 +125,11 @@
125125 </span>
126126 <span class="grow">
127127 </span>
128+<details class="menu vmenu">
129+<summary class="btn" aria-label="Diff view">
130+<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
131+</summary>
132+<div class="menu-pop" role="group" aria-label="Diff view">
128133 <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>
129134 </a>
130135 <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>
@@ -134,9 +139,11 @@
134139 <kbd aria-hidden="true">x</kbd>
135140 </button>
136141 </div>
142+</details>
143+</div>
137144 <p class="notice">this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.</p>
138145 <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff">
139-<details class="dfile lnw-2" id="f1" data-file>
146+<details class="dfile lnw-2" id="f1" data-file="big.txt">
140147 <summary>
141148 <span class="st">A</span>
142149 <span class="p">big.txt</span>
@@ -281,6 +288,12 @@
281288 </div>
282289 <div>
283290 <dt>
291+<kbd>w</kbd>
292+</dt>
293+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
294+</div>
295+<div>
296+<dt>
284297 <kbd>y</kbd>
285298 </dt>
286299 <dd>copy a permalink pinned to the commit</dd>
@@ -458,6 +471,7 @@
458471 k-split from:body -> ermite.layout('split');
459472 k-unified from:body -> ermite.layout('unified');
460473 k-collapse from:body -> ermite.collapse();
474+ k-wide from:body -> window.ermiteWide.toggle();
461475 k-expand from:body -> ermite.expandFile();
462476 k-expandAll from:body -> ermite.expandAll();
463477 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -125,6 +125,11 @@
125 </span>125 </span>
126 <span class="grow">126 <span class="grow">
127 </span>127 </span>
128+<details class="menu vmenu">
129+<summary class="btn" aria-label="Diff view">
130+<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
131+</summary>
132+<div class="menu-pop" role="group" aria-label="Diff view">
128 <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>133 <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>
129 </a>134 </a>
130 <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>135 <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>
@@ -134,9 +139,11 @@
134 <kbd aria-hidden="true">x</kbd>139 <kbd aria-hidden="true">x</kbd>
135 </button>140 </button>
136 </div>141 </div>
142+</details>
143+</div>
137 <p class="notice">this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.</p>144 <p class="notice">this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.</p>
138 <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff">145 <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff">
139-<details class="dfile lnw-2" id="f1" data-file>146+<details class="dfile lnw-2" id="f1" data-file="big.txt">
140 <summary>147 <summary>
141 <span class="st">A</span>148 <span class="st">A</span>
142 <span class="p">big.txt</span>149 <span class="p">big.txt</span>
@@ -281,6 +288,12 @@
281 </div>288 </div>
282 <div>289 <div>
283 <dt>290 <dt>
291+<kbd>w</kbd>
292+</dt>
293+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
294+</div>
295+<div>
296+<dt>
284 <kbd>y</kbd>297 <kbd>y</kbd>
285 </dt>298 </dt>
286 <dd>copy a permalink pinned to the commit</dd>299 <dd>copy a permalink pinned to the commit</dd>
@@ -458,6 +471,7 @@
458 k-split from:body -> ermite.layout('split');471 k-split from:body -> ermite.layout('split');
459 k-unified from:body -> ermite.layout('unified');472 k-unified from:body -> ermite.layout('unified');
460 k-collapse from:body -> ermite.collapse();473 k-collapse from:body -> ermite.collapse();
474+ k-wide from:body -> window.ermiteWide.toggle();
461 k-expand from:body -> ermite.expandFile();475 k-expand from:body -> ermite.expandFile();
462 k-expandAll from:body -> ermite.expandAll();476 k-expandAll from:body -> ermite.expandAll();
463 k-permalink from:body -> ermite.permalink();477 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/commit-binary.html +16 -2
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -121,6 +121,11 @@
121121 </span>
122122 <span class="grow">
123123 </span>
124+<details class="menu vmenu">
125+<summary class="btn" aria-label="Diff view">
126+<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
127+</summary>
128+<div class="menu-pop" role="group" aria-label="Diff view">
124129 <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>
125130 </a>
126131 <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>
@@ -130,8 +135,10 @@
130135 <kbd aria-hidden="true">x</kbd>
131136 </button>
132137 </div>
138+</details>
139+</div>
133140 <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff">
134-<details class="dfile lnw-2" id="f1" data-file>
141+<details class="dfile lnw-2" id="f1" data-file="logo.bin">
135142 <summary>
136143 <span class="st">M</span>
137144 <span class="p">logo.bin</span>
@@ -273,6 +280,12 @@
273280 </div>
274281 <div>
275282 <dt>
283+<kbd>w</kbd>
284+</dt>
285+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
286+</div>
287+<div>
288+<dt>
276289 <kbd>y</kbd>
277290 </dt>
278291 <dd>copy a permalink pinned to the commit</dd>
@@ -450,6 +463,7 @@
450463 k-split from:body -> ermite.layout('split');
451464 k-unified from:body -> ermite.layout('unified');
452465 k-collapse from:body -> ermite.collapse();
466+ k-wide from:body -> window.ermiteWide.toggle();
453467 k-expand from:body -> ermite.expandFile();
454468 k-expandAll from:body -> ermite.expandAll();
455469 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -121,6 +121,11 @@
121 </span>121 </span>
122 <span class="grow">122 <span class="grow">
123 </span>123 </span>
124+<details class="menu vmenu">
125+<summary class="btn" aria-label="Diff view">
126+<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
127+</summary>
128+<div class="menu-pop" role="group" aria-label="Diff view">
124 <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>129 <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>
125 </a>130 </a>
126 <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>131 <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>
@@ -130,8 +135,10 @@
130 <kbd aria-hidden="true">x</kbd>135 <kbd aria-hidden="true">x</kbd>
131 </button>136 </button>
132 </div>137 </div>
138+</details>
139+</div>
133 <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff">140 <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff">
134-<details class="dfile lnw-2" id="f1" data-file>141+<details class="dfile lnw-2" id="f1" data-file="logo.bin">
135 <summary>142 <summary>
136 <span class="st">M</span>143 <span class="st">M</span>
137 <span class="p">logo.bin</span>144 <span class="p">logo.bin</span>
@@ -273,6 +280,12 @@
273 </div>280 </div>
274 <div>281 <div>
275 <dt>282 <dt>
283+<kbd>w</kbd>
284+</dt>
285+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
286+</div>
287+<div>
288+<dt>
276 <kbd>y</kbd>289 <kbd>y</kbd>
277 </dt>290 </dt>
278 <dd>copy a permalink pinned to the commit</dd>291 <dd>copy a permalink pinned to the commit</dd>
@@ -450,6 +463,7 @@
450 k-split from:body -> ermite.layout('split');463 k-split from:body -> ermite.layout('split');
451 k-unified from:body -> ermite.layout('unified');464 k-unified from:body -> ermite.layout('unified');
452 k-collapse from:body -> ermite.collapse();465 k-collapse from:body -> ermite.collapse();
466+ k-wide from:body -> window.ermiteWide.toggle();
453 k-expand from:body -> ermite.expandFile();467 k-expand from:body -> ermite.expandFile();
454 k-expandAll from:body -> ermite.expandAll();468 k-expandAll from:body -> ermite.expandAll();
455 k-permalink from:body -> ermite.permalink();469 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/commit-merge.html +16 -2
@@ -20,7 +20,7 @@
2020 </script>
2121 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
2222 </script>
23-<script src="/-/static/js/store.HASH.js" defer>
23+<script src="/-/static/js/store.HASH.js">
2424 </script>
2525 <script src="/-/static/js/ermite.HASH.js" defer>
2626 </script>
@@ -120,6 +120,11 @@
120120 </span>
121121 <span class="grow">
122122 </span>
123+<details class="menu vmenu">
124+<summary class="btn" aria-label="Diff view">
125+<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
126+</summary>
127+<div class="menu-pop" role="group" aria-label="Diff view">
123128 <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>
124129 </a>
125130 <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>
@@ -129,8 +134,10 @@
129134 <kbd aria-hidden="true">x</kbd>
130135 </button>
131136 </div>
137+</details>
138+</div>
132139 <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff">
133-<details class="dfile lnw-2" id="f1" data-file open>
140+<details class="dfile lnw-2" id="f1" data-file="side.txt" open>
134141 <summary>
135142 <span class="st">A</span>
136143 <span class="p">side.txt</span>
@@ -315,6 +322,12 @@
315322 </div>
316323 <div>
317324 <dt>
325+<kbd>w</kbd>
326+</dt>
327+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
328+</div>
329+<div>
330+<dt>
318331 <kbd>y</kbd>
319332 </dt>
320333 <dd>copy a permalink pinned to the commit</dd>
@@ -492,6 +505,7 @@
492505 k-split from:body -> ermite.layout('split');
493506 k-unified from:body -> ermite.layout('unified');
494507 k-collapse from:body -> ermite.collapse();
508+ k-wide from:body -> window.ermiteWide.toggle();
495509 k-expand from:body -> ermite.expandFile();
496510 k-expandAll from:body -> ermite.expandAll();
497511 k-permalink from:body -> ermite.permalink();
@@ -20,7 +20,7 @@
20 </script>20 </script>
21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>21 <script src="/-/static/js/hx-sse.min.HASH.js" defer>
22 </script>22 </script>
23-<script src="/-/static/js/store.HASH.js" defer>23+<script src="/-/static/js/store.HASH.js">
24 </script>24 </script>
25 <script src="/-/static/js/ermite.HASH.js" defer>25 <script src="/-/static/js/ermite.HASH.js" defer>
26 </script>26 </script>
@@ -120,6 +120,11 @@
120 </span>120 </span>
121 <span class="grow">121 <span class="grow">
122 </span>122 </span>
123+<details class="menu vmenu">
124+<summary class="btn" aria-label="Diff view">
125+<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
126+</summary>
127+<div class="menu-pop" role="group" aria-label="Diff view">
123 <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>128 <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>
124 </a>129 </a>
125 <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>130 <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>
@@ -129,8 +134,10 @@
129 <kbd aria-hidden="true">x</kbd>134 <kbd aria-hidden="true">x</kbd>
130 </button>135 </button>
131 </div>136 </div>
137+</details>
138+</div>
132 <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff">139 <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff">
133-<details class="dfile lnw-2" id="f1" data-file open>140+<details class="dfile lnw-2" id="f1" data-file="side.txt" open>
134 <summary>141 <summary>
135 <span class="st">A</span>142 <span class="st">A</span>
136 <span class="p">side.txt</span>143 <span class="p">side.txt</span>
@@ -315,6 +322,12 @@
315 </div>322 </div>
316 <div>323 <div>
317 <dt>324 <dt>
325+<kbd>w</kbd>
326+</dt>
327+<dd>wide mode: use the whole width of the screen, or not (remembered)</dd>
328+</div>
329+<div>
330+<dt>
318 <kbd>y</kbd>331 <kbd>y</kbd>
319 </dt>332 </dt>
320 <dd>copy a permalink pinned to the commit</dd>333 <dd>copy a permalink pinned to the commit</dd>
@@ -492,6 +505,7 @@
492 k-split from:body -> ermite.layout('split');505 k-split from:body -> ermite.layout('split');
493 k-unified from:body -> ermite.layout('unified');506 k-unified from:body -> ermite.layout('unified');
494 k-collapse from:body -> ermite.collapse();507 k-collapse from:body -> ermite.collapse();
508+ k-wide from:body -> window.ermiteWide.toggle();
495 k-expand from:body -> ermite.expandFile();509 k-expand from:body -> ermite.expandFile();
496 k-expandAll from:body -> ermite.expandAll();510 k-expandAll from:body -> ermite.expandAll();
497 k-permalink from:body -> ermite.permalink();511 k-permalink from:body -> ermite.permalink();
M internal/app/testdata/web/commit-modify-split.html +17 -3
M internal/app/testdata/web/commit-modify.html +17 -3
M internal/app/testdata/web/commit-rename.html +17 -3
M internal/app/testdata/web/commit-root.html +20 -6
M internal/app/testdata/web/commit-trojan.html +16 -2
M internal/app/testdata/web/commits-page2.html +8 -1
M internal/app/testdata/web/commits-path.html +8 -1
M internal/app/testdata/web/commits-root.html +8 -1
M internal/app/testdata/web/commits-small.html +8 -1
M internal/app/testdata/web/empty.html +8 -1
M internal/app/testdata/web/home.html +40 -31
M internal/app/testdata/web/not-found-page.html +8 -1
M internal/app/testdata/web/not-found.html +8 -1
M internal/app/testdata/web/refs.html +8 -1
M internal/app/testdata/web/summary.html +8 -1
M internal/app/testdata/web/tree-nested.html +8 -1
M internal/app/testdata/web/tree-root.html +8 -1
M internal/app/testdata/web/tree-slash-ref.html +8 -1
M internal/app/testdata/web/tree-subdir.html +8 -1
M internal/app/web_events_test.go +2 -2
M internal/app/web_mr_test.go +2 -0
M internal/domain/repo.go +1 -0
M internal/ports/dto.go +1 -1
M internal/ports/mocks/RepoMetaStore.go +36 -21
M internal/ports/ports.go +7 -4
A internal/usecase/activity.go +43 -0
A internal/usecase/activity_store_test.go +46 -0
A internal/usecase/activity_test.go +47 -0
M internal/usecase/export_test.go +10 -0
M internal/usecase/issue_moderation.go +1 -0
M internal/usecase/issue_moderation_test.go +1 -0
M internal/usecase/issues.go +3 -0
M internal/usecase/issues_integration_test.go +67 -0
M internal/usecase/issues_test.go +2 -0
M internal/usecase/mergerequests.go +5 -0
M internal/usecase/mergerequests_integration_test.go +11 -0
M internal/usecase/pushes.go +21 -29
M internal/usecase/pushes_test.go +11 -16
A internal/usecase/repo_mutation_test.go +34 -0
M internal/usecase/repos_write.go +6 -14
M internal/usecase/repos_write_test.go +20 -21
M proto/ermite/storage/repo.proto +2 -0

Comments0 open / 0

no comments yet.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 2

Tags 44

↑↓ moveenter openesc close