detached fcc7703

feat(web): one merge request card, a diff view menu, a wide diff mode and quieter hunk rows

commit
fcc77036ea838fc565f571e1c6afe37cd933ecc7
author
brice · · 2026-10-19 15:05 UTC
committer
brice · · 2026-10-19 15:05 UTC
parent
d8e7d5b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
16 files · +866 -681
M internal/adapters/web/diff.templ +30 -3
@@ -72,9 +72,8 @@ templ Commit(l Layout, p CommitPage) {
7272 <div class="diff-tools" role="group" aria-label="Diff view">
7373 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
7474 <span class="grow"></span>
75- @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
76- @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
77- @collapseButton()
75+ @diffViewMenu(p.DiffView)
76+ @wideButton()
7877 </div>
7978 if p.Invisible {
8079 <p class="notice">
@@ -112,6 +111,34 @@ templ Commit(l Layout, p CommitPage) {
112111 }
113112 }
114113
114+// diffViewMenu is the layout of the diff (unified, the default, or split) and
115+// the fold of its files, in one menu: a <details> that works without scripts;
116+// u, d and x still act without opening it. Its button names the layout shown.
117+templ diffViewMenu(p DiffView) {
118+ <details class="menu vmenu">
119+ <summary class="btn" aria-label="Diff view">
120+ if p.Both {
121+ <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span>
122+ } else {
123+ { viewLabel(p.View) }
124+ }
125+ </summary>
126+ <div class="menu-pop" role="group" aria-label="Diff view">
127+ @viewLink(p, "unified", "Unified", "u", p.UnifiedHref)
128+ @viewLink(p, "split", "Split", "d", p.SplitHref)
129+ @collapseButton()
130+ </div>
131+ </details>
132+}
133+
134+// wideButton gives the diff the whole width of the screen: the rail goes to
135+// the right edge and the code takes the rest. ermite.js keeps the choice.
136+templ wideButton() {
137+ <button type="button" class="btn" data-js data-fire="wide" data-wide-toggle aria-pressed="false" aria-keyshortcuts="w" title="use the whole width of the screen">
138+ Wide<kbd aria-hidden="true">w</kbd>
139+ </button>
140+}
141+
115142 // viewLink is a button of the diff tools. It is a link, so that the split
116143 // layout works without JavaScript; with it, and when both layouts are in the
117144 // page, a click only flips the layout.
@@ -72,9 +72,8 @@ templ Commit(l Layout, p CommitPage) {
72 <div class="diff-tools" role="group" aria-label="Diff view">72 <div class="diff-tools" role="group" aria-label="Diff view">
73 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>73 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
74 <span class="grow"></span>74 <span class="grow"></span>
75- @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)75+ @diffViewMenu(p.DiffView)
76- @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)76+ @wideButton()
77- @collapseButton()
78 </div>77 </div>
79 if p.Invisible {78 if p.Invisible {
80 <p class="notice">79 <p class="notice">
@@ -112,6 +111,34 @@ templ Commit(l Layout, p CommitPage) {
112 }111 }
113 }112 }
114 113
114+// diffViewMenu is the layout of the diff (unified, the default, or split) and
115+// the fold of its files, in one menu: a <details> that works without scripts;
116+// u, d and x still act without opening it. Its button names the layout shown.
117+templ diffViewMenu(p DiffView) {
118+ <details class="menu vmenu">
119+ <summary class="btn" aria-label="Diff view">
120+ if p.Both {
121+ <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span>
122+ } else {
123+ { viewLabel(p.View) }
124+ }
125+ </summary>
126+ <div class="menu-pop" role="group" aria-label="Diff view">
127+ @viewLink(p, "unified", "Unified", "u", p.UnifiedHref)
128+ @viewLink(p, "split", "Split", "d", p.SplitHref)
129+ @collapseButton()
130+ </div>
131+ </details>
132+}
133+
134+// wideButton gives the diff the whole width of the screen: the rail goes to
135+// the right edge and the code takes the rest. ermite.js keeps the choice.
136+templ wideButton() {
137+ <button type="button" class="btn" data-js data-fire="wide" data-wide-toggle aria-pressed="false" aria-keyshortcuts="w" title="use the whole width of the screen">
138+ Wide<kbd aria-hidden="true">w</kbd>
139+ </button>
140+}
141+
115 // viewLink is a button of the diff tools. It is a link, so that the split142 // viewLink is a button of the diff tools. It is a link, so that the split
116 // layout works without JavaScript; with it, and when both layouts are in the143 // layout works without JavaScript; with it, and when both layouts are in the
117 // page, a click only flips the layout.144 // page, a click only flips the layout.
M internal/adapters/web/diff_templ.go +542 -434 generated file: collapsed
M internal/adapters/web/format.go +8 -0
@@ -61,6 +61,14 @@ func grouped(n int) string {
6161 return string(b)
6262 }
6363
64+// viewLabel names a diff layout on the button of the view menu.
65+func viewLabel(view string) string {
66+ if view == "split" {
67+ return "Split"
68+ }
69+ return "Unified"
70+}
71+
6472 func plural(n int, one, many string) string {
6573 if n == 1 {
6674 return "1 " + one
@@ -61,6 +61,14 @@ func grouped(n int) string {
61 return string(b)61 return string(b)
62 }62 }
63 63
64+// viewLabel names a diff layout on the button of the view menu.
65+func viewLabel(view string) string {
66+ if view == "split" {
67+ return "Split"
68+ }
69+ return "Unified"
70+}
71+
64 func plural(n int, one, many string) string {72 func plural(n int, one, many string) string {
65 if n == 1 {73 if n == 1 {
66 return "1 " + one74 return "1 " + one
M internal/adapters/web/issues.templ +24 -22
@@ -136,32 +136,34 @@ templ issueNewRail() {
136136 // owner, the form to comment and the button that closes or reopens it.
137137 templ IssueShow(l Layout, p IssuePage) {
138138 @Shell(l, issueRail(p)) {
139- <header class="show mr-h">
140- <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>
141- <dl class="kv show-kv">
142- <dt>state</dt>
143- <dd class="mr-state" data-state={ p.State }>{ p.State }</dd>
144- <dt>opened</dt>
145- <dd>
146- { p.Opened.Name }
147- if p.Opened.WhenAt != "" {
148- { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>
139+ <section class="mr-card">
140+ <header class="show mr-h">
141+ <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>
142+ <dl class="kv show-kv">
143+ <dt>state</dt>
144+ <dd class="mr-state" data-state={ p.State }>{ p.State }</dd>
145+ <dt>opened</dt>
146+ <dd>
147+ { p.Opened.Name }
148+ if p.Opened.WhenAt != "" {
149+ { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>
150+ }
151+ </dd>
152+ if p.UpdatedAt != "" {
153+ <dt>updated</dt>
154+ <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd>
149155 }
150- </dd>
151- if p.UpdatedAt != "" {
152- <dt>updated</dt>
153- <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd>
154- }
155- </dl>
156- </header>
156+ </dl>
157+ </header>
158+ if p.Desc != "" {
159+ <article class="mr-desc prose" aria-label="description" hx-ignore>
160+ @templ.Raw(string(p.Desc))
161+ </article>
162+ }
163+ </section>
157164 if p.Live {
158165 @LiveSlot("mr-notice")
159166 }
160- if p.Desc != "" {
161- <article class="mr-desc prose" aria-label="description" hx-ignore>
162- @templ.Raw(string(p.Desc))
163- </article>
164- }
165167 <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(len(p.Comments)) }</span></h2>
166168 for _, c := range p.Comments {
167169 @commentView(c, "")
@@ -136,32 +136,34 @@ templ issueNewRail() {
136 // owner, the form to comment and the button that closes or reopens it.136 // owner, the form to comment and the button that closes or reopens it.
137 templ IssueShow(l Layout, p IssuePage) {137 templ IssueShow(l Layout, p IssuePage) {
138 @Shell(l, issueRail(p)) {138 @Shell(l, issueRail(p)) {
139- <header class="show mr-h">139+ <section class="mr-card">
140- <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>140+ <header class="show mr-h">
141- <dl class="kv show-kv">141+ <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>
142- <dt>state</dt>142+ <dl class="kv show-kv">
143- <dd class="mr-state" data-state={ p.State }>{ p.State }</dd>143+ <dt>state</dt>
144- <dt>opened</dt>144+ <dd class="mr-state" data-state={ p.State }>{ p.State }</dd>
145- <dd>145+ <dt>opened</dt>
146- { p.Opened.Name }146+ <dd>
147- if p.Opened.WhenAt != "" {147+ { p.Opened.Name }
148- { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>148+ if p.Opened.WhenAt != "" {
149+ { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>
150+ }
151+ </dd>
152+ if p.UpdatedAt != "" {
153+ <dt>updated</dt>
154+ <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd>
149 }155 }
150- </dd>156+ </dl>
151- if p.UpdatedAt != "" {157+ </header>
152- <dt>updated</dt>158+ if p.Desc != "" {
153- <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd>159+ <article class="mr-desc prose" aria-label="description" hx-ignore>
154- }160+ @templ.Raw(string(p.Desc))
155- </dl>161+ </article>
156- </header>162+ }
163+ </section>
157 if p.Live {164 if p.Live {
158 @LiveSlot("mr-notice")165 @LiveSlot("mr-notice")
159 }166 }
160- if p.Desc != "" {
161- <article class="mr-desc prose" aria-label="description" hx-ignore>
162- @templ.Raw(string(p.Desc))
163- </article>
164- }
165 <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(len(p.Comments)) }</span></h2>167 <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(len(p.Comments)) }</span></h2>
166 for _, c := range p.Comments {168 for _, c := range p.Comments {
167 @commentView(c, "")169 @commentView(c, "")
M internal/adapters/web/issues_templ.go +26 -26
@@ -585,14 +585,14 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
585585 }()
586586 }
587587 ctx = templ.InitializeContext(ctx)
588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<header class=\"show mr-h\"><h1><span class=\"mute\">")
588+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1><span class=\"mute\">")
589589 if templ_7745c5c3_Err != nil {
590590 return templ_7745c5c3_Err
591591 }
592592 var templ_7745c5c3_Var27 string
593593 templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)
594594 if templ_7745c5c3_Err != nil {
595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 140, Col: 36}
595+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 141, Col: 37}
596596 }
597597 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
598598 if templ_7745c5c3_Err != nil {
@@ -605,7 +605,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
605605 var templ_7745c5c3_Var28 string
606606 templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
607607 if templ_7745c5c3_Err != nil {
608- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 140, Col: 55}
608+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 141, Col: 56}
609609 }
610610 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
611611 if templ_7745c5c3_Err != nil {
@@ -618,7 +618,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
618618 var templ_7745c5c3_Var29 string
619619 templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State)
620620 if templ_7745c5c3_Err != nil {
621- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 143, Col: 45}
621+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 144, Col: 46}
622622 }
623623 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
624624 if templ_7745c5c3_Err != nil {
@@ -631,7 +631,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
631631 var templ_7745c5c3_Var30 string
632632 templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)
633633 if templ_7745c5c3_Err != nil {
634- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 143, Col: 57}
634+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 144, Col: 58}
635635 }
636636 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
637637 if templ_7745c5c3_Err != nil {
@@ -644,7 +644,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
644644 var templ_7745c5c3_Var31 string
645645 templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name)
646646 if templ_7745c5c3_Err != nil {
647- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 146, Col: 20}
647+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 147, Col: 21}
648648 }
649649 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
650650 if templ_7745c5c3_Err != nil {
@@ -658,7 +658,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
658658 var templ_7745c5c3_Var32 string
659659 templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
660660 if templ_7745c5c3_Err != nil {
661- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 148, Col: 14}
661+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 15}
662662 }
663663 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
664664 if templ_7745c5c3_Err != nil {
@@ -671,7 +671,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
671671 var templ_7745c5c3_Var33 string
672672 templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt)
673673 if templ_7745c5c3_Err != nil {
674- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 148, Col: 48}
674+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 49}
675675 }
676676 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
677677 if templ_7745c5c3_Err != nil {
@@ -684,7 +684,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
684684 var templ_7745c5c3_Var34 string
685685 templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs)
686686 if templ_7745c5c3_Err != nil {
687- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 148, Col: 71}
687+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 72}
688688 }
689689 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
690690 if templ_7745c5c3_Err != nil {
@@ -697,7 +697,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
697697 var templ_7745c5c3_Var35 string
698698 templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When)
699699 if templ_7745c5c3_Err != nil {
700- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 148, Col: 89}
700+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 90}
701701 }
702702 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
703703 if templ_7745c5c3_Err != nil {
@@ -720,7 +720,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
720720 var templ_7745c5c3_Var36 string
721721 templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt)
722722 if templ_7745c5c3_Err != nil {
723- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 153, Col: 37}
723+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 154, Col: 38}
724724 }
725725 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
726726 if templ_7745c5c3_Err != nil {
@@ -733,7 +733,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
733733 var templ_7745c5c3_Var37 string
734734 templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated)
735735 if templ_7745c5c3_Err != nil {
736- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 153, Col: 51}
736+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 154, Col: 52}
737737 }
738738 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
739739 if templ_7745c5c3_Err != nil {
@@ -748,26 +748,26 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
748748 if templ_7745c5c3_Err != nil {
749749 return templ_7745c5c3_Err
750750 }
751- if p.Live {
752- templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer)
751+ if p.Desc != "" {
752+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>")
753753 if templ_7745c5c3_Err != nil {
754754 return templ_7745c5c3_Err
755755 }
756- }
757- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " ")
758- if templ_7745c5c3_Err != nil {
759- return templ_7745c5c3_Err
760- }
761- if p.Desc != "" {
762- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>")
756+ templ_7745c5c3_Err = templ.Raw(string(p.Desc)).Render(ctx, templ_7745c5c3_Buffer)
763757 if templ_7745c5c3_Err != nil {
764758 return templ_7745c5c3_Err
765759 }
766- templ_7745c5c3_Err = templ.Raw(string(p.Desc)).Render(ctx, templ_7745c5c3_Buffer)
760+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</article>")
767761 if templ_7745c5c3_Err != nil {
768762 return templ_7745c5c3_Err
769763 }
770- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</article>")
764+ }
765+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</section>")
766+ if templ_7745c5c3_Err != nil {
767+ return templ_7745c5c3_Err
768+ }
769+ if p.Live {
770+ templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer)
771771 if templ_7745c5c3_Err != nil {
772772 return templ_7745c5c3_Err
773773 }
@@ -779,7 +779,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
779779 var templ_7745c5c3_Var38 string
780780 templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Comments)))
781781 if templ_7745c5c3_Err != nil {
782- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 165, Col: 99}
782+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 167, Col: 99}
783783 }
784784 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
785785 if templ_7745c5c3_Err != nil {
@@ -854,7 +854,7 @@ func issueRail(p IssuePage) templ.Component {
854854 var templ_7745c5c3_Var40 templ.SafeURL
855855 templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Transition.Action))
856856 if templ_7745c5c3_Err != nil {
857- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 182, Col: 66}
857+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 184, Col: 66}
858858 }
859859 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
860860 if templ_7745c5c3_Err != nil {
@@ -867,7 +867,7 @@ func issueRail(p IssuePage) templ.Component {
867867 var templ_7745c5c3_Var41 string
868868 templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Transition.Label)
869869 if templ_7745c5c3_Err != nil {
870- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 183, Col: 58}
870+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 185, Col: 58}
871871 }
872872 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
873873 if templ_7745c5c3_Err != nil {
@@ -585,14 +585,14 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
585 }()585 }()
586 }586 }
587 ctx = templ.InitializeContext(ctx)587 ctx = templ.InitializeContext(ctx)
588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<header class=\"show mr-h\"><h1><span class=\"mute\">")588+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1><span class=\"mute\">")
589 if templ_7745c5c3_Err != nil {589 if templ_7745c5c3_Err != nil {
590 return templ_7745c5c3_Err590 return templ_7745c5c3_Err
591 }591 }
592 var templ_7745c5c3_Var27 string592 var templ_7745c5c3_Var27 string
593 templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)593 templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)
594 if templ_7745c5c3_Err != nil {594 if templ_7745c5c3_Err != nil {
595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 140, Col: 36}595+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 141, Col: 37}
596 }596 }
597 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))597 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
598 if templ_7745c5c3_Err != nil {598 if templ_7745c5c3_Err != nil {
@@ -605,7 +605,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
605 var templ_7745c5c3_Var28 string605 var templ_7745c5c3_Var28 string
606 templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)606 templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
607 if templ_7745c5c3_Err != nil {607 if templ_7745c5c3_Err != nil {
608- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 140, Col: 55}608+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 141, Col: 56}
609 }609 }
610 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))610 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
611 if templ_7745c5c3_Err != nil {611 if templ_7745c5c3_Err != nil {
@@ -618,7 +618,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
618 var templ_7745c5c3_Var29 string618 var templ_7745c5c3_Var29 string
619 templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State)619 templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State)
620 if templ_7745c5c3_Err != nil {620 if templ_7745c5c3_Err != nil {
621- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 143, Col: 45}621+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 144, Col: 46}
622 }622 }
623 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)623 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
624 if templ_7745c5c3_Err != nil {624 if templ_7745c5c3_Err != nil {
@@ -631,7 +631,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
631 var templ_7745c5c3_Var30 string631 var templ_7745c5c3_Var30 string
632 templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)632 templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)
633 if templ_7745c5c3_Err != nil {633 if templ_7745c5c3_Err != nil {
634- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 143, Col: 57}634+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 144, Col: 58}
635 }635 }
636 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))636 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
637 if templ_7745c5c3_Err != nil {637 if templ_7745c5c3_Err != nil {
@@ -644,7 +644,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
644 var templ_7745c5c3_Var31 string644 var templ_7745c5c3_Var31 string
645 templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name)645 templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name)
646 if templ_7745c5c3_Err != nil {646 if templ_7745c5c3_Err != nil {
647- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 146, Col: 20}647+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 147, Col: 21}
648 }648 }
649 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))649 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
650 if templ_7745c5c3_Err != nil {650 if templ_7745c5c3_Err != nil {
@@ -658,7 +658,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
658 var templ_7745c5c3_Var32 string658 var templ_7745c5c3_Var32 string
659 templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")659 templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
660 if templ_7745c5c3_Err != nil {660 if templ_7745c5c3_Err != nil {
661- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 148, Col: 14}661+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 15}
662 }662 }
663 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))663 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
664 if templ_7745c5c3_Err != nil {664 if templ_7745c5c3_Err != nil {
@@ -671,7 +671,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
671 var templ_7745c5c3_Var33 string671 var templ_7745c5c3_Var33 string
672 templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt)672 templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt)
673 if templ_7745c5c3_Err != nil {673 if templ_7745c5c3_Err != nil {
674- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 148, Col: 48}674+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 49}
675 }675 }
676 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)676 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
677 if templ_7745c5c3_Err != nil {677 if templ_7745c5c3_Err != nil {
@@ -684,7 +684,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
684 var templ_7745c5c3_Var34 string684 var templ_7745c5c3_Var34 string
685 templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs)685 templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs)
686 if templ_7745c5c3_Err != nil {686 if templ_7745c5c3_Err != nil {
687- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 148, Col: 71}687+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 72}
688 }688 }
689 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)689 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
690 if templ_7745c5c3_Err != nil {690 if templ_7745c5c3_Err != nil {
@@ -697,7 +697,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
697 var templ_7745c5c3_Var35 string697 var templ_7745c5c3_Var35 string
698 templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When)698 templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When)
699 if templ_7745c5c3_Err != nil {699 if templ_7745c5c3_Err != nil {
700- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 148, Col: 89}700+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 90}
701 }701 }
702 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))702 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
703 if templ_7745c5c3_Err != nil {703 if templ_7745c5c3_Err != nil {
@@ -720,7 +720,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
720 var templ_7745c5c3_Var36 string720 var templ_7745c5c3_Var36 string
721 templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt)721 templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt)
722 if templ_7745c5c3_Err != nil {722 if templ_7745c5c3_Err != nil {
723- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 153, Col: 37}723+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 154, Col: 38}
724 }724 }
725 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)725 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
726 if templ_7745c5c3_Err != nil {726 if templ_7745c5c3_Err != nil {
@@ -733,7 +733,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
733 var templ_7745c5c3_Var37 string733 var templ_7745c5c3_Var37 string
734 templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated)734 templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated)
735 if templ_7745c5c3_Err != nil {735 if templ_7745c5c3_Err != nil {
736- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 153, Col: 51}736+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 154, Col: 52}
737 }737 }
738 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))738 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
739 if templ_7745c5c3_Err != nil {739 if templ_7745c5c3_Err != nil {
@@ -748,26 +748,26 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
748 if templ_7745c5c3_Err != nil {748 if templ_7745c5c3_Err != nil {
749 return templ_7745c5c3_Err749 return templ_7745c5c3_Err
750 }750 }
751- if p.Live {751+ if p.Desc != "" {
752- templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer)752+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>")
753 if templ_7745c5c3_Err != nil {753 if templ_7745c5c3_Err != nil {
754 return templ_7745c5c3_Err754 return templ_7745c5c3_Err
755 }755 }
756- }756+ templ_7745c5c3_Err = templ.Raw(string(p.Desc)).Render(ctx, templ_7745c5c3_Buffer)
757- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " ")
758- if templ_7745c5c3_Err != nil {
759- return templ_7745c5c3_Err
760- }
761- if p.Desc != "" {
762- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>")
763 if templ_7745c5c3_Err != nil {757 if templ_7745c5c3_Err != nil {
764 return templ_7745c5c3_Err758 return templ_7745c5c3_Err
765 }759 }
766- templ_7745c5c3_Err = templ.Raw(string(p.Desc)).Render(ctx, templ_7745c5c3_Buffer)760+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</article>")
767 if templ_7745c5c3_Err != nil {761 if templ_7745c5c3_Err != nil {
768 return templ_7745c5c3_Err762 return templ_7745c5c3_Err
769 }763 }
770- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</article>")764+ }
765+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</section>")
766+ if templ_7745c5c3_Err != nil {
767+ return templ_7745c5c3_Err
768+ }
769+ if p.Live {
770+ templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer)
771 if templ_7745c5c3_Err != nil {771 if templ_7745c5c3_Err != nil {
772 return templ_7745c5c3_Err772 return templ_7745c5c3_Err
773 }773 }
@@ -779,7 +779,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
779 var templ_7745c5c3_Var38 string779 var templ_7745c5c3_Var38 string
780 templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Comments)))780 templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Comments)))
781 if templ_7745c5c3_Err != nil {781 if templ_7745c5c3_Err != nil {
782- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 165, Col: 99}782+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 167, Col: 99}
783 }783 }
784 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))784 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
785 if templ_7745c5c3_Err != nil {785 if templ_7745c5c3_Err != nil {
@@ -854,7 +854,7 @@ func issueRail(p IssuePage) templ.Component {
854 var templ_7745c5c3_Var40 templ.SafeURL854 var templ_7745c5c3_Var40 templ.SafeURL
855 templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Transition.Action))855 templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Transition.Action))
856 if templ_7745c5c3_Err != nil {856 if templ_7745c5c3_Err != nil {
857- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 182, Col: 66}857+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 184, Col: 66}
858 }858 }
859 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))859 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
860 if templ_7745c5c3_Err != nil {860 if templ_7745c5c3_Err != nil {
@@ -867,7 +867,7 @@ func issueRail(p IssuePage) templ.Component {
867 var templ_7745c5c3_Var41 string867 var templ_7745c5c3_Var41 string
868 templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Transition.Label)868 templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Transition.Label)
869 if templ_7745c5c3_Err != nil {869 if templ_7745c5c3_Err != nil {
870- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 183, Col: 58}870+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 185, Col: 58}
871 }871 }
872 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))872 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
873 if templ_7745c5c3_Err != nil {873 if templ_7745c5c3_Err != nil {
M internal/adapters/web/keys.templ +1 -0
@@ -32,6 +32,7 @@ templ Keys() {
3232 k-split from:body -> ermite.layout('split');
3333 k-unified from:body -> ermite.layout('unified');
3434 k-collapse from:body -> ermite.collapse();
35+ k-wide from:body -> window.ermiteWide.toggle();
3536 k-expand from:body -> ermite.expandFile();
3637 k-expandAll from:body -> ermite.expandAll();
3738 k-permalink from:body -> ermite.permalink();
@@ -32,6 +32,7 @@ templ Keys() {
32 k-split from:body -> ermite.layout('split');32 k-split from:body -> ermite.layout('split');
33 k-unified from:body -> ermite.layout('unified');33 k-unified from:body -> ermite.layout('unified');
34 k-collapse from:body -> ermite.collapse();34 k-collapse from:body -> ermite.collapse();
35+ k-wide from:body -> window.ermiteWide.toggle();
35 k-expand from:body -> ermite.expandFile();36 k-expand from:body -> ermite.expandFile();
36 k-expandAll from:body -> ermite.expandAll();37 k-expandAll from:body -> ermite.expandAll();
37 k-permalink from:body -> ermite.permalink();38 k-permalink from:body -> ermite.permalink();
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/mr.templ +39 -39
@@ -109,41 +109,43 @@ templ mrListRail(p MRListPage) {
109109 // form, the checks, the settings, the commits and the files.
110110 templ MergeRequest(l Layout, p MRPage) {
111111 @Shell(l, mrRail(p)) {
112- <header class="show mr-h">
113- <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>
114- <dl class="kv show-kv">
115- <dt>state</dt>
116- <dd class="mr-state" data-state={ p.State }>
117- { p.State }
118- if p.MergedShort != "" {
119- { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode }
120- }
121- </dd>
122- <dt>branch</dt>
123- <dd>
124- @branchLink(p.Source, p.SourceHref)
125- { " → " }
126- @branchLink(p.Target, p.TargetHref)
127- </dd>
128- <dt>opened</dt>
129- <dd>
130- { p.Opened.Name }
131- if p.Opened.WhenAt != "" {
132- { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>
112+ <section class="mr-card">
113+ <header class="show mr-h">
114+ <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>
115+ <dl class="kv show-kv">
116+ <dt>state</dt>
117+ <dd class="mr-state" data-state={ p.State }>
118+ { p.State }
119+ if p.MergedShort != "" {
120+ { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode }
121+ }
122+ </dd>
123+ <dt>branch</dt>
124+ <dd>
125+ @branchLink(p.Source, p.SourceHref)
126+ { " → " }
127+ @branchLink(p.Target, p.TargetHref)
128+ </dd>
129+ <dt>opened</dt>
130+ <dd>
131+ { p.Opened.Name }
132+ if p.Opened.WhenAt != "" {
133+ { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>
134+ }
135+ </dd>
136+ if p.UpdatedAt != "" {
137+ <dt>updated</dt>
138+ <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd>
133139 }
134- </dd>
135- if p.UpdatedAt != "" {
136- <dt>updated</dt>
137- <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd>
138- }
139- </dl>
140- </header>
140+ </dl>
141+ </header>
142+ if p.Desc != "" {
143+ <article class="mr-desc prose" aria-label="description" hx-ignore>
144+ @templ.Raw(string(p.Desc))
145+ </article>
146+ }
147+ </section>
141148 @LiveSlot("mr-notice")
142- if p.Desc != "" {
143- <article class="mr-desc prose" aria-label="description" hx-ignore>
144- @templ.Raw(string(p.Desc))
145- </article>
146- }
147149 if p.Since != nil {
148150 <nav class="since" aria-label="Diff range">
149151 <a
@@ -431,9 +433,8 @@ templ mrRail(p MRPage) {
431433 templ mrBar(p MRPage) {
432434 <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge">
433435 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
434- @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
435- @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
436- @collapseButton()
436+ @diffViewMenu(p.DiffView)
437+ @wideButton()
437438 <span class="grow"></span>
438439 @MRChecksSum(p.ChecksSum)
439440 if p.Box != nil {
@@ -562,9 +563,8 @@ templ Compare(l Layout, p ComparePage) {
562563 <div class="diff-tools" role="group" aria-label="Diff view">
563564 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
564565 <span class="grow"></span>
565- @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
566- @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
567- @collapseButton()
566+ @diffViewMenu(p.DiffView)
567+ @wideButton()
568568 </div>
569569 if p.Invisible {
570570 <p class="notice">
@@ -109,41 +109,43 @@ templ mrListRail(p MRListPage) {
109 // form, the checks, the settings, the commits and the files.109 // form, the checks, the settings, the commits and the files.
110 templ MergeRequest(l Layout, p MRPage) {110 templ MergeRequest(l Layout, p MRPage) {
111 @Shell(l, mrRail(p)) {111 @Shell(l, mrRail(p)) {
112- <header class="show mr-h">112+ <section class="mr-card">
113- <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>113+ <header class="show mr-h">
114- <dl class="kv show-kv">114+ <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>
115- <dt>state</dt>115+ <dl class="kv show-kv">
116- <dd class="mr-state" data-state={ p.State }>116+ <dt>state</dt>
117- { p.State }117+ <dd class="mr-state" data-state={ p.State }>
118- if p.MergedShort != "" {118+ { p.State }
119- { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode }119+ if p.MergedShort != "" {
120- }120+ { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode }
121- </dd>121+ }
122- <dt>branch</dt>122+ </dd>
123- <dd>123+ <dt>branch</dt>
124- @branchLink(p.Source, p.SourceHref)124+ <dd>
125- { " → " }125+ @branchLink(p.Source, p.SourceHref)
126- @branchLink(p.Target, p.TargetHref)126+ { " → " }
127- </dd>127+ @branchLink(p.Target, p.TargetHref)
128- <dt>opened</dt>128+ </dd>
129- <dd>129+ <dt>opened</dt>
130- { p.Opened.Name }130+ <dd>
131- if p.Opened.WhenAt != "" {131+ { p.Opened.Name }
132- { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>132+ if p.Opened.WhenAt != "" {
133+ { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>
134+ }
135+ </dd>
136+ if p.UpdatedAt != "" {
137+ <dt>updated</dt>
138+ <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd>
133 }139 }
134- </dd>140+ </dl>
135- if p.UpdatedAt != "" {141+ </header>
136- <dt>updated</dt>142+ if p.Desc != "" {
137- <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd>143+ <article class="mr-desc prose" aria-label="description" hx-ignore>
138- }144+ @templ.Raw(string(p.Desc))
139- </dl>145+ </article>
140- </header>146+ }
147+ </section>
141 @LiveSlot("mr-notice")148 @LiveSlot("mr-notice")
142- if p.Desc != "" {
143- <article class="mr-desc prose" aria-label="description" hx-ignore>
144- @templ.Raw(string(p.Desc))
145- </article>
146- }
147 if p.Since != nil {149 if p.Since != nil {
148 <nav class="since" aria-label="Diff range">150 <nav class="since" aria-label="Diff range">
149 <a151 <a
@@ -431,9 +433,8 @@ templ mrRail(p MRPage) {
431 templ mrBar(p MRPage) {433 templ mrBar(p MRPage) {
432 <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge">434 <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge">
433 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>435 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
434- @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)436+ @diffViewMenu(p.DiffView)
435- @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)437+ @wideButton()
436- @collapseButton()
437 <span class="grow"></span>438 <span class="grow"></span>
438 @MRChecksSum(p.ChecksSum)439 @MRChecksSum(p.ChecksSum)
439 if p.Box != nil {440 if p.Box != nil {
@@ -562,9 +563,8 @@ templ Compare(l Layout, p ComparePage) {
562 <div class="diff-tools" role="group" aria-label="Diff view">563 <div class="diff-tools" role="group" aria-label="Diff view">
563 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>564 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
564 <span class="grow"></span>565 <span class="grow"></span>
565- @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)566+ @diffViewMenu(p.DiffView)
566- @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)567+ @wideButton()
567- @collapseButton()
568 </div>568 </div>
569 if p.Invisible {569 if p.Invisible {
570 <p class="notice">570 <p class="notice">
M internal/adapters/web/mr_templ.go +127 -135 generated file: collapsed
M internal/adapters/web/overlays.templ +1 -0
@@ -30,6 +30,7 @@ templ OverlayHelp(mr bool) {
3030 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>
3131 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>
3232 <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div>
33+ <div><dt><kbd>w</kbd></dt><dd>diff: use the whole width of the screen, or not</dd></div>
3334 <div><dt><kbd>e</kbd> <kbd>E</kbd></dt><dd>diff: the whole file in view / every file, with the lines the hunks hide</dd></div>
3435 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>
3536 if mr {
@@ -30,6 +30,7 @@ templ OverlayHelp(mr bool) {
30 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>30 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>
31 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>31 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>
32 <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div>32 <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div>
33+ <div><dt><kbd>w</kbd></dt><dd>diff: use the whole width of the screen, or not</dd></div>
33 <div><dt><kbd>e</kbd> <kbd>E</kbd></dt><dd>diff: the whole file in view / every file, with the lines the hunks hide</dd></div>34 <div><dt><kbd>e</kbd> <kbd>E</kbd></dt><dd>diff: the whole file in view / every file, with the lines the hunks hide</dd></div>
34 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>35 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>
35 if mr {36 if mr {
M internal/adapters/web/overlays_templ.go +12 -12 generated file: collapsed
M internal/adapters/web/static/css/20-shell.css +3 -0
@@ -133,6 +133,9 @@
133133 align-content: start;
134134 }
135135 .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); }
136+ /* the wide diff (w, ermiteWide): a page with a diff takes the whole width
137+ of the screen; the rail goes to the right edge, the code takes the rest */
138+ html.wide body:has(#diffs) :is(.work, .hdr-top, .tabs, .foot) { max-width: none; }
136139 .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }
137140 .work-lede { grid-column: 1 / -1; margin-bottom: calc(-1 * var(--s2)); }
138141 main > * + * { margin-top: var(--s4); }
@@ -133,6 +133,9 @@
133 align-content: start;133 align-content: start;
134 }134 }
135 .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); }135 .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); }
136+ /* the wide diff (w, ermiteWide): a page with a diff takes the whole width
137+ of the screen; the rail goes to the right edge, the code takes the rest */
138+ html.wide body:has(#diffs) :is(.work, .hdr-top, .tabs, .foot) { max-width: none; }
136 .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }139 .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)); }140 .work-lede { grid-column: 1 / -1; margin-bottom: calc(-1 * var(--s2)); }
138 main > * + * { margin-top: var(--s4); }141 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: color-mix(in srgb, var(--ink-mute) 70%, transparent); 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: color-mix(in srgb, var(--ink-mute) 70%, transparent); 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/60-mr.css +15 -0
@@ -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; }
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 {
@@ -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; }
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 {
M internal/adapters/web/static/js/ermite.js +30 -1
@@ -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 diff: the page takes the whole width (ermiteWide).
1718 //
1819 // Preferences are kept by ermiteStore (store.js, loaded first).
1920 (() => {
@@ -553,9 +554,30 @@
553554 return { get: pref.get, set, apply, values };
554555 })();
555556
557+ // The wide diff: on the pages with a diff, the work area takes the whole
558+ // width of the screen, the rail at the right edge and the code in the rest.
559+ // The choice is kept by ermiteStore ("diff-wide": '1' when wide) and shown
560+ // as html.wide, which the styles follow, and as aria-pressed on the
561+ // buttons [data-wide-toggle]. Another tab's change applies here at once.
562+ window.ermiteWide = (() => {
563+ const pref = window.ermiteStore.pref('diff-wide', { values: ['1', ''], fallback: '' });
564+ function apply() {
565+ const wide = pref.get() === '1';
566+ document.documentElement.classList.toggle?.('wide', wide);
567+ for (const b of document.querySelectorAll('[data-wide-toggle]')) b.setAttribute('aria-pressed', String(wide));
568+ }
569+ function toggle() {
570+ pref.set(pref.get() === '1' ? '' : '1');
571+ apply();
572+ }
573+ pref.watch(apply);
574+ apply();
575+ return { toggle, apply };
576+ })();
577+
556578 // 1. Keys.
557579 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',
580+ 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',
559581 // the merge request page (mr.js): the merge menu, mark read
560582 M: 'merge', R: 'read' };
561583 // "g t" is the key "t": the tabs have one event each.
@@ -611,6 +633,13 @@
611633 if (!el || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
612634 e.preventDefault();
613635 ermite.fire(el.dataset.fire);
636+ // A choice in a menu closes it.
637+ const menu = el.closest('details.menu');
638+ if (menu) menu.open = false;
639+ });
640+ // A click outside an open menu closes it.
641+ addEventListener('click', (e) => {
642+ for (const d of $$('details.menu[open]')) if (!d.contains(e.target)) d.open = false;
614643 });
615644 for (const a of $$('a[data-fire="refs"]')) {
616645 a.setAttribute('aria-haspopup', 'dialog');
@@ -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 diff: the 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 (() => {
@@ -553,9 +554,30 @@
553 return { get: pref.get, set, apply, values };554 return { get: pref.get, set, apply, values };
554 })();555 })();
555 556
557+ // The wide diff: on the pages with a diff, the work area takes the whole
558+ // width of the screen, the rail at the right edge and the code in the rest.
559+ // The choice is kept by ermiteStore ("diff-wide": '1' when wide) and shown
560+ // as html.wide, which the styles follow, and as aria-pressed on the
561+ // buttons [data-wide-toggle]. Another tab's change applies here at once.
562+ window.ermiteWide = (() => {
563+ const pref = window.ermiteStore.pref('diff-wide', { values: ['1', ''], fallback: '' });
564+ function apply() {
565+ const wide = pref.get() === '1';
566+ document.documentElement.classList.toggle?.('wide', wide);
567+ for (const b of document.querySelectorAll('[data-wide-toggle]')) b.setAttribute('aria-pressed', String(wide));
568+ }
569+ function toggle() {
570+ pref.set(pref.get() === '1' ? '' : '1');
571+ apply();
572+ }
573+ pref.watch(apply);
574+ apply();
575+ return { toggle, apply };
576+ })();
577+
556 // 1. Keys.578 // 1. Keys.
557 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',579 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',580+ 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 read581 // the merge request page (mr.js): the merge menu, mark read
560 M: 'merge', R: 'read' };582 M: 'merge', R: 'read' };
561 // "g t" is the key "t": the tabs have one event each.583 // "g t" is the key "t": the tabs have one event each.
@@ -611,6 +633,13 @@
611 if (!el || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;633 if (!el || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
612 e.preventDefault();634 e.preventDefault();
613 ermite.fire(el.dataset.fire);635 ermite.fire(el.dataset.fire);
636+ // A choice in a menu closes it.
637+ const menu = el.closest('details.menu');
638+ if (menu) menu.open = false;
639+ });
640+ // A click outside an open menu closes it.
641+ addEventListener('click', (e) => {
642+ for (const d of $$('details.menu[open]')) if (!d.contains(e.target)) d.open = false;
614 });643 });
615 for (const a of $$('a[data-fire="refs"]')) {644 for (const a of $$('a[data-fire="refs"]')) {
616 a.setAttribute('aria-haspopup', 'dialog');645 a.setAttribute('aria-haspopup', 'dialog');
M internal/adapters/web/static/js/mr.js +0 -4
@@ -311,10 +311,6 @@ window.ermiteMR = (() => {
311311 try { sessionStorage.setItem('mr-last-line', lastLine); } catch { /* storage optional */ }
312312 if (next) { e.preventDefault(); location.assign(next); }
313313 });
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- });
318314 }
319315
320316 return { core, start, load, reveal, openMerge, closeMenus, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile };
@@ -311,10 +311,6 @@ window.ermiteMR = (() => {
311 try { sessionStorage.setItem('mr-last-line', lastLine); } catch { /* storage optional */ }311 try { sessionStorage.setItem('mr-last-line', lastLine); } catch { /* storage optional */ }
312 if (next) { e.preventDefault(); location.assign(next); }312 if (next) { e.preventDefault(); location.assign(next); }
313 });313 });
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 }314 }
319 315
320 return { core, start, load, reveal, openMerge, closeMenus, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile };316 return { core, start, load, reveal, openMerge, closeMenus, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile };

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
esc
close a dialog, leave a field

Branches 3

Tags 43

↑↓ moveenter openesc close