detached a8cc2eb

feat(web): show resolved issues and linked issue history

commit
a8cc2eb2e4df41b3fd7ca24493d3cbf47d20e8c4
author
brice · · 2026-10-22 18:14 UTC
committer
brice · · 2026-10-22 18:41 UTC
parent
38857c6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
16 files · +1643 -915
M internal/adapters/web/fragments.go +5 -1
@@ -67,7 +67,7 @@ func liveEvent(we ports.WatchEvent) bool {
6767 domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,
6868 domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.MergeRequestAutoMergeEnabled, domain.MergeRequestAutoMergeCancelled, domain.MergeRequestMergeability, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
6969 domain.RunUpdated, domain.JobUpdated,
70- domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved, domain.IssueReferenced:
70+ domain.IssueOpened, domain.IssueResolved, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved, domain.IssueReferenced:
7171 return true
7272 }
7373 return false
@@ -129,6 +129,8 @@ func (l *liveStream) home(ctx context.Context, we ports.WatchEvent) ([]fragment,
129129 name, fresh = ev.Repo.String(), true
130130 case domain.IssueOpened:
131131 name, fresh = ev.Issue.Repo.String(), true
132+ case domain.IssueResolved:
133+ name, fresh = ev.Issue.Repo.String(), true
132134 case domain.IssueClosed:
133135 name, fresh = ev.Issue.Repo.String(), true
134136 case domain.IssueReopened:
@@ -239,6 +241,8 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
239241 return l.mrNotice(ctx, ev.Number, "a comment was added — reload")
240242 case domain.IssueOpened:
241243 return l.issueNotice(ctx, ev.Issue.Number, "an issue was opened — reload")
244+ case domain.IssueResolved:
245+ return l.issueNotice(ctx, ev.Issue.Number, "this issue was resolved — reload")
242246 case domain.IssueClosed:
243247 return l.issueNotice(ctx, ev.Issue.Number, "this issue was closed — reload")
244248 case domain.IssueReopened:
@@ -67,7 +67,7 @@ func liveEvent(we ports.WatchEvent) bool {
67 domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,67 domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,
68 domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.MergeRequestAutoMergeEnabled, domain.MergeRequestAutoMergeCancelled, domain.MergeRequestMergeability, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,68 domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.MergeRequestAutoMergeEnabled, domain.MergeRequestAutoMergeCancelled, domain.MergeRequestMergeability, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
69 domain.RunUpdated, domain.JobUpdated,69 domain.RunUpdated, domain.JobUpdated,
70- domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved, domain.IssueReferenced:70+ domain.IssueOpened, domain.IssueResolved, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved, domain.IssueReferenced:
71 return true71 return true
72 }72 }
73 return false73 return false
@@ -129,6 +129,8 @@ func (l *liveStream) home(ctx context.Context, we ports.WatchEvent) ([]fragment,
129 name, fresh = ev.Repo.String(), true129 name, fresh = ev.Repo.String(), true
130 case domain.IssueOpened:130 case domain.IssueOpened:
131 name, fresh = ev.Issue.Repo.String(), true131 name, fresh = ev.Issue.Repo.String(), true
132+ case domain.IssueResolved:
133+ name, fresh = ev.Issue.Repo.String(), true
132 case domain.IssueClosed:134 case domain.IssueClosed:
133 name, fresh = ev.Issue.Repo.String(), true135 name, fresh = ev.Issue.Repo.String(), true
134 case domain.IssueReopened:136 case domain.IssueReopened:
@@ -239,6 +241,8 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
239 return l.mrNotice(ctx, ev.Number, "a comment was added — reload")241 return l.mrNotice(ctx, ev.Number, "a comment was added — reload")
240 case domain.IssueOpened:242 case domain.IssueOpened:
241 return l.issueNotice(ctx, ev.Issue.Number, "an issue was opened — reload")243 return l.issueNotice(ctx, ev.Issue.Number, "an issue was opened — reload")
244+ case domain.IssueResolved:
245+ return l.issueNotice(ctx, ev.Issue.Number, "this issue was resolved — reload")
242 case domain.IssueClosed:246 case domain.IssueClosed:
243 return l.issueNotice(ctx, ev.Issue.Number, "this issue was closed — reload")247 return l.issueNotice(ctx, ev.Issue.Number, "this issue was closed — reload")
244 case domain.IssueReopened:248 case domain.IssueReopened:
M internal/adapters/web/home_test.go +1 -1
@@ -379,7 +379,7 @@ func TestActivityEventsRefreshTheHomeRow(t *testing.T) {
379379 domain.MergeRequestOpened{MergeRequest: mr}, domain.MergeRequestMerged{MergeRequest: mr},
380380 domain.MergeRequestClosed{MergeRequest: mr}, domain.MergeRequestReady{MergeRequest: mr},
381381 domain.CommentAdded{Repo: repo.Name}, domain.IssueOpened{Issue: issue},
382- domain.IssueClosed{Issue: issue}, domain.IssueReopened{Issue: issue}, domain.IssueCommented{Repo: repo.Name},
382+ domain.IssueResolved{Issue: issue}, domain.IssueClosed{Issue: issue}, domain.IssueReopened{Issue: issue}, domain.IssueCommented{Repo: repo.Name},
383383 } {
384384 t.Run(ev.EventKind(), func(t *testing.T) {
385385 t.Parallel()
@@ -379,7 +379,7 @@ func TestActivityEventsRefreshTheHomeRow(t *testing.T) {
379 domain.MergeRequestOpened{MergeRequest: mr}, domain.MergeRequestMerged{MergeRequest: mr},379 domain.MergeRequestOpened{MergeRequest: mr}, domain.MergeRequestMerged{MergeRequest: mr},
380 domain.MergeRequestClosed{MergeRequest: mr}, domain.MergeRequestReady{MergeRequest: mr},380 domain.MergeRequestClosed{MergeRequest: mr}, domain.MergeRequestReady{MergeRequest: mr},
381 domain.CommentAdded{Repo: repo.Name}, domain.IssueOpened{Issue: issue},381 domain.CommentAdded{Repo: repo.Name}, domain.IssueOpened{Issue: issue},
382- domain.IssueClosed{Issue: issue}, domain.IssueReopened{Issue: issue}, domain.IssueCommented{Repo: repo.Name},382+ domain.IssueResolved{Issue: issue}, domain.IssueClosed{Issue: issue}, domain.IssueReopened{Issue: issue}, domain.IssueCommented{Repo: repo.Name},
383 } {383 } {
384 t.Run(ev.EventKind(), func(t *testing.T) {384 t.Run(ev.EventKind(), func(t *testing.T) {
385 t.Parallel()385 t.Parallel()
M internal/adapters/web/inbox.go +1 -1
@@ -287,7 +287,7 @@ func (s *Server) inboxCount(ctx context.Context) (n int, show bool) {
287287 func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {
288288 switch we.Event.(type) {
289289 case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
290- domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved,
290+ domain.IssueOpened, domain.IssueResolved, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved,
291291 domain.RepoUpdated, domain.RepoDeleted, domain.RunUpdated:
292292 default:
293293 return nil, nil
@@ -287,7 +287,7 @@ func (s *Server) inboxCount(ctx context.Context) (n int, show bool) {
287 func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {287 func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {
288 switch we.Event.(type) {288 switch we.Event.(type) {
289 case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,289 case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
290- domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved,290+ domain.IssueOpened, domain.IssueResolved, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved,
291 domain.RepoUpdated, domain.RepoDeleted, domain.RunUpdated:291 domain.RepoUpdated, domain.RepoDeleted, domain.RunUpdated:
292 default:292 default:
293 return nil, nil293 return nil, nil
M internal/adapters/web/inbox_test.go +5 -0
@@ -188,6 +188,11 @@ func TestInboxLiveFragments(t *testing.T) {
188188 assert.Contains(t, frags[4].html, `id="inbox-mrs"`)
189189 assert.Contains(t, frags[5].html, `id="inbox-issues"`)
190190
191+ resolved, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.IssueResolved{Issue: domain.Issue{Repo: repoName(t, "beta"), Number: 7}}})
192+ require.NoError(t, err)
193+ require.Len(t, resolved, 6)
194+ assert.Contains(t, resolved[5].html, `id="inbox-issues"`)
195+
191196 none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})
192197 require.NoError(t, err)
193198 assert.Empty(t, none, "a push is not inbox news")
@@ -188,6 +188,11 @@ func TestInboxLiveFragments(t *testing.T) {
188 assert.Contains(t, frags[4].html, `id="inbox-mrs"`)188 assert.Contains(t, frags[4].html, `id="inbox-mrs"`)
189 assert.Contains(t, frags[5].html, `id="inbox-issues"`)189 assert.Contains(t, frags[5].html, `id="inbox-issues"`)
190 190
191+ resolved, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.IssueResolved{Issue: domain.Issue{Repo: repoName(t, "beta"), Number: 7}}})
192+ require.NoError(t, err)
193+ require.Len(t, resolved, 6)
194+ assert.Contains(t, resolved[5].html, `id="inbox-issues"`)
195+
191 none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})196 none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})
192 require.NoError(t, err)197 require.NoError(t, err)
193 assert.Empty(t, none, "a push is not inbox news")198 assert.Empty(t, none, "a push is not inbox news")
A internal/adapters/web/issue_timeline.go +92 -0
@@ -0,0 +1,92 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "sort"
7+ "strconv"
8+ "time"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+)
13+
14+// TimelineItem holds either a comment or a compact system event.
15+type TimelineItem struct {
16+ Comment *CommentView
17+ Event *IssueEventView
18+ at time.Time
19+}
20+
21+type IssueEventView struct {
22+ ID, Kind, Actor, Text, When, WhenAt, Abs string
23+ MR *Link
24+ Closes bool
25+ Icon string
26+}
27+
28+type IssueLinkView struct {
29+ Number, Title, State, Href string
30+ Closes bool
31+}
32+
33+// issueTimeline keeps comments ahead of events with the same timestamp.
34+func (s *Server) issueTimeline(repo string, comments []CommentView, events []ports.IssueEventDetail, mr bool) []TimelineItem {
35+ items := make([]TimelineItem, 0, len(comments)+len(events))
36+ for _, c := range comments {
37+ at := c.createdAt
38+ items = append(items, TimelineItem{Comment: &c, at: at})
39+ }
40+ for _, detail := range events {
41+ e := detail.Event
42+ v := IssueEventView{ID: strconv.FormatInt(e.ID, 10), Kind: string(e.Kind), Actor: s.who(e.Actor), Closes: e.Closes}
43+ v.When, v.WhenAt = whenOf(s.cfg.Now(), e.CreatedAt)
44+ if !e.CreatedAt.IsZero() {
45+ v.Abs = absolute(e.CreatedAt)
46+ }
47+ switch e.Kind {
48+ case domain.IssueEventReferenced:
49+ v.Text, v.Icon = "referenced this in", "↗"
50+ if e.Closes {
51+ v.Text = "will resolve this in"
52+ }
53+ case domain.IssueEventClosed:
54+ v.Text, v.Icon = "closed this", "✗"
55+ case domain.IssueEventReopened:
56+ v.Text, v.Icon = "reopened this", "↻"
57+ case domain.IssueEventResolved:
58+ v.Text, v.Icon = "resolved this", "✓"
59+ if e.MergeRequest != 0 {
60+ v.Text += " with"
61+ }
62+ default:
63+ continue
64+ }
65+ if mr {
66+ // IDs are per issue, so MR event anchors also include the issue number.
67+ v.ID = strconv.FormatInt(e.Issue, 10) + "-" + v.ID
68+ v.MR = &Link{Label: "#" + strconv.FormatInt(e.Issue, 10), Href: issueHref(repo, e.Issue)}
69+ switch e.Kind {
70+ case domain.IssueEventReferenced:
71+ v.Text = "referenced"
72+ if e.Closes {
73+ v.Text = "will resolve"
74+ }
75+ case domain.IssueEventResolved:
76+ v.Text = "resolved"
77+ case domain.IssueEventClosed:
78+ v.Text = "closed"
79+ case domain.IssueEventReopened:
80+ v.Text = "reopened"
81+ }
82+ } else if e.MergeRequest != 0 {
83+ v.MR = numberLink(repo, e.MergeRequest)
84+ if title := visibleText(detail.MergeRequestTitle.String()); title != "" {
85+ v.MR.Label += " " + title
86+ }
87+ }
88+ items = append(items, TimelineItem{Event: &v, at: e.CreatedAt})
89+ }
90+ sort.SliceStable(items, func(i, j int) bool { return items[i].at.Before(items[j].at) })
91+ return items
92+}
@@ -0,0 +1,92 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "sort"
7+ "strconv"
8+ "time"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+)
13+
14+// TimelineItem holds either a comment or a compact system event.
15+type TimelineItem struct {
16+ Comment *CommentView
17+ Event *IssueEventView
18+ at time.Time
19+}
20+
21+type IssueEventView struct {
22+ ID, Kind, Actor, Text, When, WhenAt, Abs string
23+ MR *Link
24+ Closes bool
25+ Icon string
26+}
27+
28+type IssueLinkView struct {
29+ Number, Title, State, Href string
30+ Closes bool
31+}
32+
33+// issueTimeline keeps comments ahead of events with the same timestamp.
34+func (s *Server) issueTimeline(repo string, comments []CommentView, events []ports.IssueEventDetail, mr bool) []TimelineItem {
35+ items := make([]TimelineItem, 0, len(comments)+len(events))
36+ for _, c := range comments {
37+ at := c.createdAt
38+ items = append(items, TimelineItem{Comment: &c, at: at})
39+ }
40+ for _, detail := range events {
41+ e := detail.Event
42+ v := IssueEventView{ID: strconv.FormatInt(e.ID, 10), Kind: string(e.Kind), Actor: s.who(e.Actor), Closes: e.Closes}
43+ v.When, v.WhenAt = whenOf(s.cfg.Now(), e.CreatedAt)
44+ if !e.CreatedAt.IsZero() {
45+ v.Abs = absolute(e.CreatedAt)
46+ }
47+ switch e.Kind {
48+ case domain.IssueEventReferenced:
49+ v.Text, v.Icon = "referenced this in", "↗"
50+ if e.Closes {
51+ v.Text = "will resolve this in"
52+ }
53+ case domain.IssueEventClosed:
54+ v.Text, v.Icon = "closed this", "✗"
55+ case domain.IssueEventReopened:
56+ v.Text, v.Icon = "reopened this", "↻"
57+ case domain.IssueEventResolved:
58+ v.Text, v.Icon = "resolved this", "✓"
59+ if e.MergeRequest != 0 {
60+ v.Text += " with"
61+ }
62+ default:
63+ continue
64+ }
65+ if mr {
66+ // IDs are per issue, so MR event anchors also include the issue number.
67+ v.ID = strconv.FormatInt(e.Issue, 10) + "-" + v.ID
68+ v.MR = &Link{Label: "#" + strconv.FormatInt(e.Issue, 10), Href: issueHref(repo, e.Issue)}
69+ switch e.Kind {
70+ case domain.IssueEventReferenced:
71+ v.Text = "referenced"
72+ if e.Closes {
73+ v.Text = "will resolve"
74+ }
75+ case domain.IssueEventResolved:
76+ v.Text = "resolved"
77+ case domain.IssueEventClosed:
78+ v.Text = "closed"
79+ case domain.IssueEventReopened:
80+ v.Text = "reopened"
81+ }
82+ } else if e.MergeRequest != 0 {
83+ v.MR = numberLink(repo, e.MergeRequest)
84+ if title := visibleText(detail.MergeRequestTitle.String()); title != "" {
85+ v.MR.Label += " " + title
86+ }
87+ }
88+ items = append(items, TimelineItem{Event: &v, at: e.CreatedAt})
89+ }
90+ sort.SliceStable(items, func(i, j int) bool { return items[i].at.Before(items[j].at) })
91+ return items
92+}
A internal/adapters/web/issue_timeline.templ +34 -0
@@ -0,0 +1,34 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+templ issueTimeline(items []TimelineItem) {
6+ if len(items) > 0 {
7+ <ol class="timeline" aria-label="Conversation history">
8+ for _, item := range items {
9+ <li>
10+ if item.Comment != nil {
11+ @commentView(*item.Comment, "")
12+ } else if item.Event != nil {
13+ @issueEventRow(*item.Event)
14+ }
15+ </li>
16+ }
17+ </ol>
18+ }
19+}
20+
21+templ issueEventRow(e IssueEventView) {
22+ <div class="sys" id={ "e" + e.ID } data-kind={ e.Kind }>
23+ <span class="sys-icon" aria-hidden="true">{ e.Icon }</span>
24+ <span>
25+ { e.Actor } { e.Text }
26+ if e.MR != nil {
27+ { " " }<a class="ref" href={ templ.SafeURL(e.MR.Href) }>{ e.MR.Label }</a>
28+ }
29+ </span>
30+ if e.WhenAt != "" {
31+ { " · " }<time datetime={ e.WhenAt } title={ e.Abs }>{ e.When }</time>
32+ }
33+ </div>
34+}
@@ -0,0 +1,34 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+templ issueTimeline(items []TimelineItem) {
6+ if len(items) > 0 {
7+ <ol class="timeline" aria-label="Conversation history">
8+ for _, item := range items {
9+ <li>
10+ if item.Comment != nil {
11+ @commentView(*item.Comment, "")
12+ } else if item.Event != nil {
13+ @issueEventRow(*item.Event)
14+ }
15+ </li>
16+ }
17+ </ol>
18+ }
19+}
20+
21+templ issueEventRow(e IssueEventView) {
22+ <div class="sys" id={ "e" + e.ID } data-kind={ e.Kind }>
23+ <span class="sys-icon" aria-hidden="true">{ e.Icon }</span>
24+ <span>
25+ { e.Actor } { e.Text }
26+ if e.MR != nil {
27+ { " " }<a class="ref" href={ templ.SafeURL(e.MR.Href) }>{ e.MR.Label }</a>
28+ }
29+ </span>
30+ if e.WhenAt != "" {
31+ { " · " }<time datetime={ e.WhenAt } title={ e.Abs }>{ e.When }</time>
32+ }
33+ </div>
34+}
A internal/adapters/web/issue_timeline_templ.go +266 -0 generated file: collapsed
M internal/adapters/web/issues.go +18 -14
@@ -25,7 +25,7 @@ import (
2525 //
2626 // Routes:
2727 //
28-// GET /<repo>/issues the list, open or closed (?state=), paged (?page=)
28+// GET /<repo>/issues the list, open, closed or resolved (?state=), paged (?page=)
2929 // GET /<repo>/issues/new the form that opens one (owner)
3030 // GET /<repo>/issues/<n> the issue with its comments
3131 //
@@ -55,11 +55,11 @@ type IssueRow struct {
5555 type IssueListPage struct {
5656 Rows []IssueRow
5757 // Closed is true on the tab of the closed issues.
58- Closed bool
59- OpenHref, ClosedHref string
60- Older, Newest string
61- NewHref string // the form that opens an issue; "" unless the owner
62- Live bool
58+ Closed, Resolved bool
59+ OpenHref, ClosedHref, ResolvedHref string
60+ Older, Newest string
61+ NewHref string // the form that opens an issue; "" unless the owner
62+ Live bool
6363 }
6464
6565 // IssueActions are the buttons of the owner on an issue.
@@ -80,11 +80,12 @@ type IssuePage struct {
8080 // Desc is the body rendered as Markdown, "" when it is empty.
8181 Desc template.HTML
8282 Comments []CommentView
83+ Timeline []TimelineItem
8384 Form *CommentForm
8485 Transition *IssueActions
8586 Live bool
86- // ClosedBy is the merge request whose merge closed the issue.
87- ClosedBy *Link
87+ // ResolvedBy is the merge request whose merge resolved the issue.
88+ ResolvedBy *Link
8889 // References are the merge requests naming the issue.
8990 References []IssueRefView
9091 }
@@ -192,6 +193,8 @@ func (s *Server) issueList(ctx context.Context, r *http.Request) (templ.Componen
192193 case "", "open":
193194 case "closed":
194195 state = domain.IssueStateClosed
196+ case "resolved":
197+ state = domain.IssueStateResolved
195198 default:
196199 return nil, 0, errNotFound
197200 }
@@ -200,8 +203,8 @@ func (s *Server) issueList(ctx context.Context, r *http.Request) (templ.Componen
200203 closed := state == domain.IssueStateClosed
201204 link := func(page string) string {
202205 q := url.Values{}
203- if closed {
204- q.Set("state", "closed")
206+ if state != domain.IssueStateOpen {
207+ q.Set("state", string(state))
205208 }
206209 if page != "" {
207210 q.Set("page", page)
@@ -218,7 +221,7 @@ func (s *Server) issueList(ctx context.Context, r *http.Request) (templ.Componen
218221 if err != nil {
219222 return nil, 0, err
220223 }
221- p := IssueListPage{Closed: closed, OpenHref: base, ClosedHref: base + "?state=closed"}
224+ p := IssueListPage{Closed: closed, OpenHref: base, ClosedHref: base + "?state=closed", Resolved: state == domain.IssueStateResolved, ResolvedHref: base + "?state=resolved"}
222225 for _, i := range res.Issues {
223226 p.Rows = append(p.Rows, s.issueRow(d.name(), i))
224227 }
@@ -293,7 +296,7 @@ func (s *Server) issueShow(ctx context.Context, r *http.Request) (templ.Componen
293296 if p.Desc, err = s.issueBody(ctx, d, i); err != nil {
294297 return nil, 0, err
295298 }
296- p.ClosedBy = numberLink(d.name(), i.ClosedByMergeRequest)
299+ p.ResolvedBy = numberLink(d.name(), i.ClosedByMergeRequest)
297300 p.References = issueRefViews(d.name(), res.References)
298301 p.Opened.Name = s.who(i.Author)
299302 if i.External {
@@ -307,7 +310,7 @@ func (s *Server) issueShow(ctx context.Context, r *http.Request) (templ.Componen
307310 p.Updated, p.UpdatedAt = whenOf(now, i.UpdatedAt)
308311 for _, c := range res.Comments {
309312 v := CommentView{
310- ID: strconv.FormatInt(c.ID, 10), Author: s.who(c.Author), Parts: plainParts(c.Text.String()),
313+ createdAt: c.CreatedAt, ID: strconv.FormatInt(c.ID, 10), Author: s.who(c.Author), Parts: plainParts(c.Text.String()),
311314 }
312315 v.When, v.WhenAt = whenOf(now, c.CreatedAt)
313316 if !c.CreatedAt.IsZero() {
@@ -315,12 +318,13 @@ func (s *Server) issueShow(ctx context.Context, r *http.Request) (templ.Componen
315318 }
316319 p.Comments = append(p.Comments, v)
317320 }
321+ p.Timeline = s.issueTimeline(d.name(), p.Comments, res.Events, false)
318322 if signed {
319323 p.Form = &CommentForm{ID: "cf-general", Action: issueHref(d.name(), n, "comments"), Title: "comment on the issue", Plain: true}
320324 switch i.State {
321325 case domain.IssueStateOpen:
322326 p.Transition = &IssueActions{Action: issueHref(d.name(), n, "close"), Label: "Close issue"}
323- case domain.IssueStateClosed:
327+ case domain.IssueStateClosed, domain.IssueStateResolved:
324328 p.Transition = &IssueActions{Action: issueHref(d.name(), n, "reopen"), Label: "Reopen issue"}
325329 }
326330 }
@@ -25,7 +25,7 @@ import (
25 //25 //
26 // Routes:26 // Routes:
27 //27 //
28-// GET /<repo>/issues the list, open or closed (?state=), paged (?page=)28+// GET /<repo>/issues the list, open, closed or resolved (?state=), paged (?page=)
29 // GET /<repo>/issues/new the form that opens one (owner)29 // GET /<repo>/issues/new the form that opens one (owner)
30 // GET /<repo>/issues/<n> the issue with its comments30 // GET /<repo>/issues/<n> the issue with its comments
31 //31 //
@@ -55,11 +55,11 @@ type IssueRow struct {
55 type IssueListPage struct {55 type IssueListPage struct {
56 Rows []IssueRow56 Rows []IssueRow
57 // Closed is true on the tab of the closed issues.57 // Closed is true on the tab of the closed issues.
58- Closed bool58+ Closed, Resolved bool
59- OpenHref, ClosedHref string59+ OpenHref, ClosedHref, ResolvedHref string
60- Older, Newest string60+ Older, Newest string
61- NewHref string // the form that opens an issue; "" unless the owner61+ NewHref string // the form that opens an issue; "" unless the owner
62- Live bool62+ Live bool
63 }63 }
64 64
65 // IssueActions are the buttons of the owner on an issue.65 // IssueActions are the buttons of the owner on an issue.
@@ -80,11 +80,12 @@ type IssuePage struct {
80 // Desc is the body rendered as Markdown, "" when it is empty.80 // Desc is the body rendered as Markdown, "" when it is empty.
81 Desc template.HTML81 Desc template.HTML
82 Comments []CommentView82 Comments []CommentView
83+ Timeline []TimelineItem
83 Form *CommentForm84 Form *CommentForm
84 Transition *IssueActions85 Transition *IssueActions
85 Live bool86 Live bool
86- // ClosedBy is the merge request whose merge closed the issue.87+ // ResolvedBy is the merge request whose merge resolved the issue.
87- ClosedBy *Link88+ ResolvedBy *Link
88 // References are the merge requests naming the issue.89 // References are the merge requests naming the issue.
89 References []IssueRefView90 References []IssueRefView
90 }91 }
@@ -192,6 +193,8 @@ func (s *Server) issueList(ctx context.Context, r *http.Request) (templ.Componen
192 case "", "open":193 case "", "open":
193 case "closed":194 case "closed":
194 state = domain.IssueStateClosed195 state = domain.IssueStateClosed
196+ case "resolved":
197+ state = domain.IssueStateResolved
195 default:198 default:
196 return nil, 0, errNotFound199 return nil, 0, errNotFound
197 }200 }
@@ -200,8 +203,8 @@ func (s *Server) issueList(ctx context.Context, r *http.Request) (templ.Componen
200 closed := state == domain.IssueStateClosed203 closed := state == domain.IssueStateClosed
201 link := func(page string) string {204 link := func(page string) string {
202 q := url.Values{}205 q := url.Values{}
203- if closed {206+ if state != domain.IssueStateOpen {
204- q.Set("state", "closed")207+ q.Set("state", string(state))
205 }208 }
206 if page != "" {209 if page != "" {
207 q.Set("page", page)210 q.Set("page", page)
@@ -218,7 +221,7 @@ func (s *Server) issueList(ctx context.Context, r *http.Request) (templ.Componen
218 if err != nil {221 if err != nil {
219 return nil, 0, err222 return nil, 0, err
220 }223 }
221- p := IssueListPage{Closed: closed, OpenHref: base, ClosedHref: base + "?state=closed"}224+ p := IssueListPage{Closed: closed, OpenHref: base, ClosedHref: base + "?state=closed", Resolved: state == domain.IssueStateResolved, ResolvedHref: base + "?state=resolved"}
222 for _, i := range res.Issues {225 for _, i := range res.Issues {
223 p.Rows = append(p.Rows, s.issueRow(d.name(), i))226 p.Rows = append(p.Rows, s.issueRow(d.name(), i))
224 }227 }
@@ -293,7 +296,7 @@ func (s *Server) issueShow(ctx context.Context, r *http.Request) (templ.Componen
293 if p.Desc, err = s.issueBody(ctx, d, i); err != nil {296 if p.Desc, err = s.issueBody(ctx, d, i); err != nil {
294 return nil, 0, err297 return nil, 0, err
295 }298 }
296- p.ClosedBy = numberLink(d.name(), i.ClosedByMergeRequest)299+ p.ResolvedBy = numberLink(d.name(), i.ClosedByMergeRequest)
297 p.References = issueRefViews(d.name(), res.References)300 p.References = issueRefViews(d.name(), res.References)
298 p.Opened.Name = s.who(i.Author)301 p.Opened.Name = s.who(i.Author)
299 if i.External {302 if i.External {
@@ -307,7 +310,7 @@ func (s *Server) issueShow(ctx context.Context, r *http.Request) (templ.Componen
307 p.Updated, p.UpdatedAt = whenOf(now, i.UpdatedAt)310 p.Updated, p.UpdatedAt = whenOf(now, i.UpdatedAt)
308 for _, c := range res.Comments {311 for _, c := range res.Comments {
309 v := CommentView{312 v := CommentView{
310- ID: strconv.FormatInt(c.ID, 10), Author: s.who(c.Author), Parts: plainParts(c.Text.String()),313+ createdAt: c.CreatedAt, ID: strconv.FormatInt(c.ID, 10), Author: s.who(c.Author), Parts: plainParts(c.Text.String()),
311 }314 }
312 v.When, v.WhenAt = whenOf(now, c.CreatedAt)315 v.When, v.WhenAt = whenOf(now, c.CreatedAt)
313 if !c.CreatedAt.IsZero() {316 if !c.CreatedAt.IsZero() {
@@ -315,12 +318,13 @@ func (s *Server) issueShow(ctx context.Context, r *http.Request) (templ.Componen
315 }318 }
316 p.Comments = append(p.Comments, v)319 p.Comments = append(p.Comments, v)
317 }320 }
321+ p.Timeline = s.issueTimeline(d.name(), p.Comments, res.Events, false)
318 if signed {322 if signed {
319 p.Form = &CommentForm{ID: "cf-general", Action: issueHref(d.name(), n, "comments"), Title: "comment on the issue", Plain: true}323 p.Form = &CommentForm{ID: "cf-general", Action: issueHref(d.name(), n, "comments"), Title: "comment on the issue", Plain: true}
320 switch i.State {324 switch i.State {
321 case domain.IssueStateOpen:325 case domain.IssueStateOpen:
322 p.Transition = &IssueActions{Action: issueHref(d.name(), n, "close"), Label: "Close issue"}326 p.Transition = &IssueActions{Action: issueHref(d.name(), n, "close"), Label: "Close issue"}
323- case domain.IssueStateClosed:327+ case domain.IssueStateClosed, domain.IssueStateResolved:
324 p.Transition = &IssueActions{Action: issueHref(d.name(), n, "reopen"), Label: "Reopen issue"}328 p.Transition = &IssueActions{Action: issueHref(d.name(), n, "reopen"), Label: "Reopen issue"}
325 }329 }
326 }330 }
M internal/adapters/web/issues.templ +21 -11
@@ -11,7 +11,7 @@ templ IssueList(l Layout, p IssueListPage) {
1111 <nav class="since" aria-label="State">
1212 <a
1313 href={ templ.SafeURL(p.OpenHref) }
14- if !p.Closed {
14+ if !p.Closed && !p.Resolved {
1515 aria-current="page"
1616 }
1717 >open</a>
@@ -21,6 +21,12 @@ templ IssueList(l Layout, p IssueListPage) {
2121 aria-current="page"
2222 }
2323 >closed</a>
24+ <a
25+ href={ templ.SafeURL(p.ResolvedHref) }
26+ if p.Resolved {
27+ aria-current="page"
28+ }
29+ >resolved</a>
2430 </nav>
2531 <div class="filter">
2632 <label for="issue-filter" class="sr">filter</label>
@@ -34,7 +40,9 @@ templ IssueList(l Layout, p IssueListPage) {
3440 />
3541 </div>
3642 <h2 class="rule-h">
37- if p.Closed {
43+ if p.Resolved {
44+ <span>Resolved</span>
45+ } else if p.Closed {
3846 <span>Closed</span>
3947 } else {
4048 <span>Open</span>
@@ -43,7 +51,9 @@ templ IssueList(l Layout, p IssueListPage) {
4351 </h2>
4452 @issueRows(p.Rows)
4553 if len(p.Rows) == 0 {
46- if p.Closed {
54+ if p.Resolved {
55+ <p class="lede dim">no resolved issues.</p>
56+ } else if p.Closed {
4757 <p class="lede dim">no closed issues.</p>
4858 } else {
4959 <p class="lede dim">no open issues.</p>
@@ -95,7 +105,9 @@ templ issueListRail(p IssueListPage) {
95105 <dt>open</dt>
96106 <dd>being worked on</dd>
97107 <dt>closed</dt>
98- <dd>done with, can be reopened</dd>
108+ <dd>closed without resolution</dd>
109+ <dt>resolved</dt>
110+ <dd>fixed, can be reopened</dd>
99111 </dl>
100112 </section>
101113 if p.NewHref != "" {
@@ -149,9 +161,9 @@ templ IssueShow(l Layout, p IssuePage) {
149161 { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>
150162 }
151163 </dd>
152- if p.ClosedBy != nil {
153- <dt>closed by</dt>
154- <dd><a class="ref" href={ templ.SafeURL(p.ClosedBy.Href) }>{ p.ClosedBy.Label }</a></dd>
164+ if p.ResolvedBy != nil {
165+ <dt>resolved by</dt>
166+ <dd><a class="ref" href={ templ.SafeURL(p.ResolvedBy.Href) }>{ p.ResolvedBy.Label }</a></dd>
155167 }
156168 if p.UpdatedAt != "" {
157169 <dt>updated</dt>
@@ -169,9 +181,7 @@ templ IssueShow(l Layout, p IssuePage) {
169181 @LiveSlot("mr-notice")
170182 }
171183 <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(len(p.Comments)) }</span></h2>
172- for _, c := range p.Comments {
173- @commentView(c, "")
174- }
184+ @issueTimeline(p.Timeline)
175185 if len(p.Comments) == 0 {
176186 <p class="dim">no comments yet.</p>
177187 }
@@ -192,7 +202,7 @@ templ issueRail(p IssuePage) {
192202 { " " + r.Title + " " }
193203 <span class="mr-state" data-state={ r.State }>{ r.State }</span>
194204 if r.Closes {
195- { " · closes" }
205+ { " · resolves" }
196206 }
197207 </li>
198208 }
@@ -11,7 +11,7 @@ templ IssueList(l Layout, p IssueListPage) {
11 <nav class="since" aria-label="State">11 <nav class="since" aria-label="State">
12 <a12 <a
13 href={ templ.SafeURL(p.OpenHref) }13 href={ templ.SafeURL(p.OpenHref) }
14- if !p.Closed {14+ if !p.Closed && !p.Resolved {
15 aria-current="page"15 aria-current="page"
16 }16 }
17 >open</a>17 >open</a>
@@ -21,6 +21,12 @@ templ IssueList(l Layout, p IssueListPage) {
21 aria-current="page"21 aria-current="page"
22 }22 }
23 >closed</a>23 >closed</a>
24+ <a
25+ href={ templ.SafeURL(p.ResolvedHref) }
26+ if p.Resolved {
27+ aria-current="page"
28+ }
29+ >resolved</a>
24 </nav>30 </nav>
25 <div class="filter">31 <div class="filter">
26 <label for="issue-filter" class="sr">filter</label>32 <label for="issue-filter" class="sr">filter</label>
@@ -34,7 +40,9 @@ templ IssueList(l Layout, p IssueListPage) {
34 />40 />
35 </div>41 </div>
36 <h2 class="rule-h">42 <h2 class="rule-h">
37- if p.Closed {43+ if p.Resolved {
44+ <span>Resolved</span>
45+ } else if p.Closed {
38 <span>Closed</span>46 <span>Closed</span>
39 } else {47 } else {
40 <span>Open</span>48 <span>Open</span>
@@ -43,7 +51,9 @@ templ IssueList(l Layout, p IssueListPage) {
43 </h2>51 </h2>
44 @issueRows(p.Rows)52 @issueRows(p.Rows)
45 if len(p.Rows) == 0 {53 if len(p.Rows) == 0 {
46- if p.Closed {54+ if p.Resolved {
55+ <p class="lede dim">no resolved issues.</p>
56+ } else if p.Closed {
47 <p class="lede dim">no closed issues.</p>57 <p class="lede dim">no closed issues.</p>
48 } else {58 } else {
49 <p class="lede dim">no open issues.</p>59 <p class="lede dim">no open issues.</p>
@@ -95,7 +105,9 @@ templ issueListRail(p IssueListPage) {
95 <dt>open</dt>105 <dt>open</dt>
96 <dd>being worked on</dd>106 <dd>being worked on</dd>
97 <dt>closed</dt>107 <dt>closed</dt>
98- <dd>done with, can be reopened</dd>108+ <dd>closed without resolution</dd>
109+ <dt>resolved</dt>
110+ <dd>fixed, can be reopened</dd>
99 </dl>111 </dl>
100 </section>112 </section>
101 if p.NewHref != "" {113 if p.NewHref != "" {
@@ -149,9 +161,9 @@ templ IssueShow(l Layout, p IssuePage) {
149 { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>161 { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time>
150 }162 }
151 </dd>163 </dd>
152- if p.ClosedBy != nil {164+ if p.ResolvedBy != nil {
153- <dt>closed by</dt>165+ <dt>resolved by</dt>
154- <dd><a class="ref" href={ templ.SafeURL(p.ClosedBy.Href) }>{ p.ClosedBy.Label }</a></dd>166+ <dd><a class="ref" href={ templ.SafeURL(p.ResolvedBy.Href) }>{ p.ResolvedBy.Label }</a></dd>
155 }167 }
156 if p.UpdatedAt != "" {168 if p.UpdatedAt != "" {
157 <dt>updated</dt>169 <dt>updated</dt>
@@ -169,9 +181,7 @@ templ IssueShow(l Layout, p IssuePage) {
169 @LiveSlot("mr-notice")181 @LiveSlot("mr-notice")
170 }182 }
171 <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(len(p.Comments)) }</span></h2>183 <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(len(p.Comments)) }</span></h2>
172- for _, c := range p.Comments {184+ @issueTimeline(p.Timeline)
173- @commentView(c, "")
174- }
175 if len(p.Comments) == 0 {185 if len(p.Comments) == 0 {
176 <p class="dim">no comments yet.</p>186 <p class="dim">no comments yet.</p>
177 }187 }
@@ -192,7 +202,7 @@ templ issueRail(p IssuePage) {
192 { " " + r.Title + " " }202 { " " + r.Title + " " }
193 <span class="mr-state" data-state={ r.State }>{ r.State }</span>203 <span class="mr-state" data-state={ r.State }>{ r.State }</span>
194 if r.Closes {204 if r.Closes {
195- { " · closes" }205+ { " · resolves" }
196 }206 }
197 </li>207 </li>
198 }208 }
M internal/adapters/web/issues_templ.go +288 -257
@@ -67,7 +67,7 @@ func IssueList(l Layout, p IssueListPage) templ.Component {
6767 if templ_7745c5c3_Err != nil {
6868 return templ_7745c5c3_Err
6969 }
70- if !p.Closed {
70+ if !p.Closed && !p.Resolved {
7171 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " aria-current=\"page\"")
7272 if templ_7745c5c3_Err != nil {
7373 return templ_7745c5c3_Err
@@ -96,35 +96,63 @@ func IssueList(l Layout, p IssueListPage) templ.Component {
9696 return templ_7745c5c3_Err
9797 }
9898 }
99- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">closed</a></nav><div class=\"filter\"><label for=\"issue-filter\" class=\"sr\">filter</label> <input id=\"issue-filter\" type=\"search\" data-filter=\".mrs tbody tr\" placeholder=\"Filter by title, author or state…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><h2 class=\"rule-h\">")
99+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">closed</a> <a href=\"")
100100 if templ_7745c5c3_Err != nil {
101101 return templ_7745c5c3_Err
102102 }
103- if p.Closed {
104- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span>Closed</span> ")
103+ var templ_7745c5c3_Var5 templ.SafeURL
104+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.ResolvedHref))
105+ if templ_7745c5c3_Err != nil {
106+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 25, Col: 40}
107+ }
108+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
109+ if templ_7745c5c3_Err != nil {
110+ return templ_7745c5c3_Err
111+ }
112+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"")
113+ if templ_7745c5c3_Err != nil {
114+ return templ_7745c5c3_Err
115+ }
116+ if p.Resolved {
117+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " aria-current=\"page\"")
118+ if templ_7745c5c3_Err != nil {
119+ return templ_7745c5c3_Err
120+ }
121+ }
122+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ">resolved</a></nav><div class=\"filter\"><label for=\"issue-filter\" class=\"sr\">filter</label> <input id=\"issue-filter\" type=\"search\" data-filter=\".mrs tbody tr\" placeholder=\"Filter by title, author or state…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><h2 class=\"rule-h\">")
123+ if templ_7745c5c3_Err != nil {
124+ return templ_7745c5c3_Err
125+ }
126+ if p.Resolved {
127+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span>Resolved</span> ")
128+ if templ_7745c5c3_Err != nil {
129+ return templ_7745c5c3_Err
130+ }
131+ } else if p.Closed {
132+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span>Closed</span> ")
105133 if templ_7745c5c3_Err != nil {
106134 return templ_7745c5c3_Err
107135 }
108136 } else {
109- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span>Open</span> ")
137+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span>Open</span> ")
110138 if templ_7745c5c3_Err != nil {
111139 return templ_7745c5c3_Err
112140 }
113141 }
114- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"count\">")
142+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span class=\"count\">")
115143 if templ_7745c5c3_Err != nil {
116144 return templ_7745c5c3_Err
117145 }
118- var templ_7745c5c3_Var5 string
119- templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))
146+ var templ_7745c5c3_Var6 string
147+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))
120148 if templ_7745c5c3_Err != nil {
121- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 42, Col: 42}
149+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 50, Col: 42}
122150 }
123- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
151+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
124152 if templ_7745c5c3_Err != nil {
125153 return templ_7745c5c3_Err
126154 }
127- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></h2>")
155+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></h2>")
128156 if templ_7745c5c3_Err != nil {
129157 return templ_7745c5c3_Err
130158 }
@@ -132,81 +160,86 @@ func IssueList(l Layout, p IssueListPage) templ.Component {
132160 if templ_7745c5c3_Err != nil {
133161 return templ_7745c5c3_Err
134162 }
135- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ")
163+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " ")
136164 if templ_7745c5c3_Err != nil {
137165 return templ_7745c5c3_Err
138166 }
139167 if len(p.Rows) == 0 {
140- if p.Closed {
141- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<p class=\"lede dim\">no closed issues.</p>")
168+ if p.Resolved {
169+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<p class=\"lede dim\">no resolved issues.</p>")
170+ if templ_7745c5c3_Err != nil {
171+ return templ_7745c5c3_Err
172+ }
173+ } else if p.Closed {
174+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<p class=\"lede dim\">no closed issues.</p>")
142175 if templ_7745c5c3_Err != nil {
143176 return templ_7745c5c3_Err
144177 }
145178 } else {
146- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<p class=\"lede dim\">no open issues.</p>")
179+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<p class=\"lede dim\">no open issues.</p>")
147180 if templ_7745c5c3_Err != nil {
148181 return templ_7745c5c3_Err
149182 }
150183 }
151184 }
152- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ")
185+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ")
153186 if templ_7745c5c3_Err != nil {
154187 return templ_7745c5c3_Err
155188 }
156189 if p.Older != "" || p.Newest != "" {
157- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<nav class=\"pager\" aria-label=\"Pages\">")
190+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<nav class=\"pager\" aria-label=\"Pages\">")
158191 if templ_7745c5c3_Err != nil {
159192 return templ_7745c5c3_Err
160193 }
161194 if p.Newest != "" {
162- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<a class=\"btn\" href=\"")
195+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<a class=\"btn\" href=\"")
163196 if templ_7745c5c3_Err != nil {
164197 return templ_7745c5c3_Err
165198 }
166- var templ_7745c5c3_Var6 templ.SafeURL
167- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Newest))
199+ var templ_7745c5c3_Var7 templ.SafeURL
200+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Newest))
168201 if templ_7745c5c3_Err != nil {
169- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 55, Col: 50}
202+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 65, Col: 50}
170203 }
171- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
204+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
172205 if templ_7745c5c3_Err != nil {
173206 return templ_7745c5c3_Err
174207 }
175- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">← Newest</a> ")
208+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">← Newest</a> ")
176209 if templ_7745c5c3_Err != nil {
177210 return templ_7745c5c3_Err
178211 }
179212 } else {
180- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"btn\" aria-disabled=\"true\">← Newest</span> ")
213+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<span class=\"btn\" aria-disabled=\"true\">← Newest</span> ")
181214 if templ_7745c5c3_Err != nil {
182215 return templ_7745c5c3_Err
183216 }
184217 }
185218 if p.Older != "" {
186- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<a class=\"btn\" href=\"")
219+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<a class=\"btn\" href=\"")
187220 if templ_7745c5c3_Err != nil {
188221 return templ_7745c5c3_Err
189222 }
190- var templ_7745c5c3_Var7 templ.SafeURL
191- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Older))
223+ var templ_7745c5c3_Var8 templ.SafeURL
224+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Older))
192225 if templ_7745c5c3_Err != nil {
193- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 60, Col: 49}
226+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 70, Col: 49}
194227 }
195- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
228+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
196229 if templ_7745c5c3_Err != nil {
197230 return templ_7745c5c3_Err
198231 }
199- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" rel=\"next\">Older →</a>")
232+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" rel=\"next\">Older →</a>")
200233 if templ_7745c5c3_Err != nil {
201234 return templ_7745c5c3_Err
202235 }
203236 } else {
204- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"btn\" aria-disabled=\"true\">Older →</span>")
237+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<span class=\"btn\" aria-disabled=\"true\">Older →</span>")
205238 if templ_7745c5c3_Err != nil {
206239 return templ_7745c5c3_Err
207240 }
208241 }
209- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</nav>")
242+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</nav>")
210243 if templ_7745c5c3_Err != nil {
211244 return templ_7745c5c3_Err
212245 }
@@ -237,160 +270,160 @@ func issueRows(rows []IssueRow) templ.Component {
237270 }()
238271 }
239272 ctx = templ.InitializeContext(ctx)
240- templ_7745c5c3_Var8 := templ.GetChildren(ctx)
241- if templ_7745c5c3_Var8 == nil {
242- templ_7745c5c3_Var8 = templ.NopComponent
273+ templ_7745c5c3_Var9 := templ.GetChildren(ctx)
274+ if templ_7745c5c3_Var9 == nil {
275+ templ_7745c5c3_Var9 = templ.NopComponent
243276 }
244277 ctx = templ.ClearChildren(ctx)
245278 if len(rows) > 0 {
246- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<table class=\"list mrs\" aria-label=\"Issues\"><tbody data-rows>")
279+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<table class=\"list mrs\" aria-label=\"Issues\"><tbody data-rows>")
247280 if templ_7745c5c3_Err != nil {
248281 return templ_7745c5c3_Err
249282 }
250283 for _, i := range rows {
251- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<tr")
284+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<tr")
252285 if templ_7745c5c3_Err != nil {
253286 return templ_7745c5c3_Err
254287 }
255288 if i.Cursor {
256- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " data-cursor")
289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " data-cursor")
257290 if templ_7745c5c3_Err != nil {
258291 return templ_7745c5c3_Err
259292 }
260293 }
261- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " data-state=\"")
294+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " data-state=\"")
262295 if templ_7745c5c3_Err != nil {
263296 return templ_7745c5c3_Err
264297 }
265- var templ_7745c5c3_Var9 string
266- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.State)
298+ var templ_7745c5c3_Var10 string
299+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.State)
267300 if templ_7745c5c3_Err != nil {
268- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 74, Col: 55}
301+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 84, Col: 55}
269302 }
270- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
303+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
271304 if templ_7745c5c3_Err != nil {
272305 return templ_7745c5c3_Err
273306 }
274- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\"><td class=\"c-n mute\">")
307+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"><td class=\"c-n mute\">")
275308 if templ_7745c5c3_Err != nil {
276309 return templ_7745c5c3_Err
277310 }
278- var templ_7745c5c3_Var10 string
279- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(i.Number)
311+ var templ_7745c5c3_Var11 string
312+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(i.Number)
280313 if templ_7745c5c3_Err != nil {
281- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 75, Col: 37}
314+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 85, Col: 37}
282315 }
283- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
316+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
284317 if templ_7745c5c3_Err != nil {
285318 return templ_7745c5c3_Err
286319 }
287- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td><td class=\"c-state\">")
320+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</td><td class=\"c-state\">")
288321 if templ_7745c5c3_Err != nil {
289322 return templ_7745c5c3_Err
290323 }
291- var templ_7745c5c3_Var11 string
292- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(i.State)
324+ var templ_7745c5c3_Var12 string
325+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(i.State)
293326 if templ_7745c5c3_Err != nil {
294- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 76, Col: 35}
327+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 86, Col: 35}
295328 }
296- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
329+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
297330 if templ_7745c5c3_Err != nil {
298331 return templ_7745c5c3_Err
299332 }
300- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td><td class=\"c-subject\"><a href=\"")
333+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</td><td class=\"c-subject\"><a href=\"")
301334 if templ_7745c5c3_Err != nil {
302335 return templ_7745c5c3_Err
303336 }
304- var templ_7745c5c3_Var12 templ.SafeURL
305- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(i.Href))
337+ var templ_7745c5c3_Var13 templ.SafeURL
338+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(i.Href))
306339 if templ_7745c5c3_Err != nil {
307- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 77, Col: 59}
340+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 87, Col: 59}
308341 }
309- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
342+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
310343 if templ_7745c5c3_Err != nil {
311344 return templ_7745c5c3_Err
312345 }
313- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">")
346+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">")
314347 if templ_7745c5c3_Err != nil {
315348 return templ_7745c5c3_Err
316349 }
317- var templ_7745c5c3_Var13 string
318- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(i.Title)
350+ var templ_7745c5c3_Var14 string
351+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(i.Title)
319352 if templ_7745c5c3_Err != nil {
320- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 77, Col: 71}
353+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 87, Col: 71}
321354 }
322- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
355+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
323356 if templ_7745c5c3_Err != nil {
324357 return templ_7745c5c3_Err
325358 }
326- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</a></td><td class=\"c-who dim\">")
359+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</a></td><td class=\"c-who dim\">")
327360 if templ_7745c5c3_Err != nil {
328361 return templ_7745c5c3_Err
329362 }
330- var templ_7745c5c3_Var14 string
331- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(i.Author)
363+ var templ_7745c5c3_Var15 string
364+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(i.Author)
332365 if templ_7745c5c3_Err != nil {
333- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 78, Col: 38}
366+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 88, Col: 38}
334367 }
335- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
368+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
336369 if templ_7745c5c3_Err != nil {
337370 return templ_7745c5c3_Err
338371 }
339- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</td><td class=\"c-when nowrap dim\">")
372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</td><td class=\"c-when nowrap dim\">")
340373 if templ_7745c5c3_Err != nil {
341374 return templ_7745c5c3_Err
342375 }
343376 if i.WhenAt != "" {
344- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<time datetime=\"")
377+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<time datetime=\"")
345378 if templ_7745c5c3_Err != nil {
346379 return templ_7745c5c3_Err
347380 }
348- var templ_7745c5c3_Var15 string
349- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.WhenAt)
381+ var templ_7745c5c3_Var16 string
382+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.WhenAt)
350383 if templ_7745c5c3_Err != nil {
351- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 81, Col: 33}
384+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 91, Col: 33}
352385 }
353- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
386+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
354387 if templ_7745c5c3_Err != nil {
355388 return templ_7745c5c3_Err
356389 }
357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" title=\"")
390+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" title=\"")
358391 if templ_7745c5c3_Err != nil {
359392 return templ_7745c5c3_Err
360393 }
361- var templ_7745c5c3_Var16 string
362- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.WhenAt)
394+ var templ_7745c5c3_Var17 string
395+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.WhenAt)
363396 if templ_7745c5c3_Err != nil {
364- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 81, Col: 52}
397+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 91, Col: 52}
365398 }
366- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
399+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
367400 if templ_7745c5c3_Err != nil {
368401 return templ_7745c5c3_Err
369402 }
370- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">")
403+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">")
371404 if templ_7745c5c3_Err != nil {
372405 return templ_7745c5c3_Err
373406 }
374- var templ_7745c5c3_Var17 string
375- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(i.When)
407+ var templ_7745c5c3_Var18 string
408+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(i.When)
376409 if templ_7745c5c3_Err != nil {
377- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 81, Col: 63}
410+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 91, Col: 63}
378411 }
379- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
412+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
380413 if templ_7745c5c3_Err != nil {
381414 return templ_7745c5c3_Err
382415 }
383- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</time>")
416+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</time>")
384417 if templ_7745c5c3_Err != nil {
385418 return templ_7745c5c3_Err
386419 }
387420 }
388- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</td></tr>")
421+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</td></tr>")
389422 if templ_7745c5c3_Err != nil {
390423 return templ_7745c5c3_Err
391424 }
392425 }
393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</tbody></table>")
426+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</tbody></table>")
394427 if templ_7745c5c3_Err != nil {
395428 return templ_7745c5c3_Err
396429 }
@@ -415,30 +448,30 @@ func issueListRail(p IssueListPage) templ.Component {
415448 }()
416449 }
417450 ctx = templ.InitializeContext(ctx)
418- templ_7745c5c3_Var18 := templ.GetChildren(ctx)
419- if templ_7745c5c3_Var18 == nil {
420- templ_7745c5c3_Var18 = templ.NopComponent
451+ templ_7745c5c3_Var19 := templ.GetChildren(ctx)
452+ if templ_7745c5c3_Var19 == nil {
453+ templ_7745c5c3_Var19 = templ.NopComponent
421454 }
422455 ctx = templ.ClearChildren(ctx)
423- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<section aria-labelledby=\"r-states\"><h2 id=\"r-states\">States</h2><dl class=\"kv\"><dt>open</dt><dd>being worked on</dd><dt>closed</dt><dd>done with, can be reopened</dd></dl></section>")
456+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<section aria-labelledby=\"r-states\"><h2 id=\"r-states\">States</h2><dl class=\"kv\"><dt>open</dt><dd>being worked on</dd><dt>closed</dt><dd>closed without resolution</dd><dt>resolved</dt><dd>fixed, can be reopened</dd></dl></section>")
424457 if templ_7745c5c3_Err != nil {
425458 return templ_7745c5c3_Err
426459 }
427460 if p.NewHref != "" {
428- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<section aria-labelledby=\"r-new\"><h2 id=\"r-new\">Open one</h2><a class=\"btn primary\" href=\"")
461+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<section aria-labelledby=\"r-new\"><h2 id=\"r-new\">Open one</h2><a class=\"btn primary\" href=\"")
429462 if templ_7745c5c3_Err != nil {
430463 return templ_7745c5c3_Err
431464 }
432- var templ_7745c5c3_Var19 templ.SafeURL
433- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.NewHref))
465+ var templ_7745c5c3_Var20 templ.SafeURL
466+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.NewHref))
434467 if templ_7745c5c3_Err != nil {
435- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 104, Col: 57}
468+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 116, Col: 57}
436469 }
437- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
470+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
438471 if templ_7745c5c3_Err != nil {
439472 return templ_7745c5c3_Err
440473 }
441- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\">New issue</a></section>")
474+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">New issue</a></section>")
442475 if templ_7745c5c3_Err != nil {
443476 return templ_7745c5c3_Err
444477 }
@@ -464,12 +497,12 @@ func IssueNew(l Layout, p IssueNewPage) templ.Component {
464497 }()
465498 }
466499 ctx = templ.InitializeContext(ctx)
467- templ_7745c5c3_Var20 := templ.GetChildren(ctx)
468- if templ_7745c5c3_Var20 == nil {
469- templ_7745c5c3_Var20 = templ.NopComponent
500+ templ_7745c5c3_Var21 := templ.GetChildren(ctx)
501+ if templ_7745c5c3_Var21 == nil {
502+ templ_7745c5c3_Var21 = templ.NopComponent
470503 }
471504 ctx = templ.ClearChildren(ctx)
472- templ_7745c5c3_Var21 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
505+ templ_7745c5c3_Var22 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
473506 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
474507 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
475508 if !templ_7745c5c3_IsBuffer {
@@ -481,39 +514,39 @@ func IssueNew(l Layout, p IssueNewPage) templ.Component {
481514 }()
482515 }
483516 ctx = templ.InitializeContext(ctx)
484- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<header class=\"show mr-h\"><h1>New issue</h1></header><form class=\"cform create\" method=\"post\" action=\"")
517+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<header class=\"show mr-h\"><h1>New issue</h1></header><form class=\"cform create\" method=\"post\" action=\"")
485518 if templ_7745c5c3_Err != nil {
486519 return templ_7745c5c3_Err
487520 }
488- var templ_7745c5c3_Var22 templ.SafeURL
489- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))
521+ var templ_7745c5c3_Var23 templ.SafeURL
522+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))
490523 if templ_7745c5c3_Err != nil {
491- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 115, Col: 75}
524+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 127, Col: 75}
492525 }
493- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
526+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
494527 if templ_7745c5c3_Err != nil {
495528 return templ_7745c5c3_Err
496529 }
497- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"><label for=\"issue-title\">title</label> <input id=\"issue-title\" name=\"title\" type=\"text\" maxlength=\"120\" required autocomplete=\"off\" autofocus> <label for=\"issue-body\">description</label> <textarea id=\"issue-body\" name=\"body\" rows=\"8\" maxlength=\"10000\" placeholder=\"Markdown, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create issue</button> <a class=\"btn\" href=\"")
530+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\"><label for=\"issue-title\">title</label> <input id=\"issue-title\" name=\"title\" type=\"text\" maxlength=\"120\" required autocomplete=\"off\" autofocus> <label for=\"issue-body\">description</label> <textarea id=\"issue-body\" name=\"body\" rows=\"8\" maxlength=\"10000\" placeholder=\"Markdown, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create issue</button> <a class=\"btn\" href=\"")
498531 if templ_7745c5c3_Err != nil {
499532 return templ_7745c5c3_Err
500533 }
501- var templ_7745c5c3_Var23 templ.SafeURL
502- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Cancel))
534+ var templ_7745c5c3_Var24 templ.SafeURL
535+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Cancel))
503536 if templ_7745c5c3_Err != nil {
504- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 122, Col: 49}
537+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 134, Col: 49}
505538 }
506- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
539+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
507540 if templ_7745c5c3_Err != nil {
508541 return templ_7745c5c3_Err
509542 }
510- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">Cancel</a></div></form>")
543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\">Cancel</a></div></form>")
511544 if templ_7745c5c3_Err != nil {
512545 return templ_7745c5c3_Err
513546 }
514547 return nil
515548 })
516- templ_7745c5c3_Err = Shell(l, issueNewRail()).Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer)
549+ templ_7745c5c3_Err = Shell(l, issueNewRail()).Render(templ.WithChildren(ctx, templ_7745c5c3_Var22), templ_7745c5c3_Buffer)
517550 if templ_7745c5c3_Err != nil {
518551 return templ_7745c5c3_Err
519552 }
@@ -537,12 +570,12 @@ func issueNewRail() templ.Component {
537570 }()
538571 }
539572 ctx = templ.InitializeContext(ctx)
540- templ_7745c5c3_Var24 := templ.GetChildren(ctx)
541- if templ_7745c5c3_Var24 == nil {
542- templ_7745c5c3_Var24 = templ.NopComponent
573+ templ_7745c5c3_Var25 := templ.GetChildren(ctx)
574+ if templ_7745c5c3_Var25 == nil {
575+ templ_7745c5c3_Var25 = templ.NopComponent
543576 }
544577 ctx = templ.ClearChildren(ctx)
545- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<section aria-labelledby=\"r-about\"><h2 id=\"r-about\">Issues</h2><p class=\"dim\">issues share their numbers with the merge requests of the repository.</p></section>")
578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<section aria-labelledby=\"r-about\"><h2 id=\"r-about\">Issues</h2><p class=\"dim\">issues share their numbers with the merge requests of the repository.</p></section>")
546579 if templ_7745c5c3_Err != nil {
547580 return templ_7745c5c3_Err
548581 }
@@ -568,12 +601,12 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
568601 }()
569602 }
570603 ctx = templ.InitializeContext(ctx)
571- templ_7745c5c3_Var25 := templ.GetChildren(ctx)
572- if templ_7745c5c3_Var25 == nil {
573- templ_7745c5c3_Var25 = templ.NopComponent
604+ templ_7745c5c3_Var26 := templ.GetChildren(ctx)
605+ if templ_7745c5c3_Var26 == nil {
606+ templ_7745c5c3_Var26 = templ.NopComponent
574607 }
575608 ctx = templ.ClearChildren(ctx)
576- templ_7745c5c3_Var26 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
609+ templ_7745c5c3_Var27 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
577610 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
578611 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
579612 if !templ_7745c5c3_IsBuffer {
@@ -585,203 +618,203 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
585618 }()
586619 }
587620 ctx = templ.InitializeContext(ctx)
588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1><span class=\"mute\">")
621+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1><span class=\"mute\">")
589622 if templ_7745c5c3_Err != nil {
590623 return templ_7745c5c3_Err
591624 }
592- var templ_7745c5c3_Var27 string
593- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)
625+ var templ_7745c5c3_Var28 string
626+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)
594627 if templ_7745c5c3_Err != nil {
595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 141, Col: 37}
628+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 153, Col: 37}
596629 }
597- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
630+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
598631 if templ_7745c5c3_Err != nil {
599632 return templ_7745c5c3_Err
600633 }
601- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span> ")
634+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span> ")
602635 if templ_7745c5c3_Err != nil {
603636 return templ_7745c5c3_Err
604637 }
605- var templ_7745c5c3_Var28 string
606- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
638+ var templ_7745c5c3_Var29 string
639+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
607640 if templ_7745c5c3_Err != nil {
608- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 141, Col: 56}
641+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 153, Col: 56}
609642 }
610- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
643+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
611644 if templ_7745c5c3_Err != nil {
612645 return templ_7745c5c3_Err
613646 }
614- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</h1><dl class=\"kv show-kv\"><dt>state</dt><dd class=\"mr-state\" data-state=\"")
647+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</h1><dl class=\"kv show-kv\"><dt>state</dt><dd class=\"mr-state\" data-state=\"")
615648 if templ_7745c5c3_Err != nil {
616649 return templ_7745c5c3_Err
617650 }
618- var templ_7745c5c3_Var29 string
619- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State)
651+ var templ_7745c5c3_Var30 string
652+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State)
620653 if templ_7745c5c3_Err != nil {
621- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 144, Col: 46}
654+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 156, Col: 46}
622655 }
623- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
656+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
624657 if templ_7745c5c3_Err != nil {
625658 return templ_7745c5c3_Err
626659 }
627- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\">")
660+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">")
628661 if templ_7745c5c3_Err != nil {
629662 return templ_7745c5c3_Err
630663 }
631- var templ_7745c5c3_Var30 string
632- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)
664+ var templ_7745c5c3_Var31 string
665+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)
633666 if templ_7745c5c3_Err != nil {
634- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 144, Col: 58}
667+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 156, Col: 58}
635668 }
636- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
669+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
637670 if templ_7745c5c3_Err != nil {
638671 return templ_7745c5c3_Err
639672 }
640- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</dd><dt>opened</dt><dd>")
673+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</dd><dt>opened</dt><dd>")
641674 if templ_7745c5c3_Err != nil {
642675 return templ_7745c5c3_Err
643676 }
644- var templ_7745c5c3_Var31 string
645- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name)
677+ var templ_7745c5c3_Var32 string
678+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name)
646679 if templ_7745c5c3_Err != nil {
647- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 147, Col: 21}
680+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 159, Col: 21}
648681 }
649- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
682+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
650683 if templ_7745c5c3_Err != nil {
651684 return templ_7745c5c3_Err
652685 }
653- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " ")
686+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " ")
654687 if templ_7745c5c3_Err != nil {
655688 return templ_7745c5c3_Err
656689 }
657690 if p.Opened.WhenAt != "" {
658- var templ_7745c5c3_Var32 string
659- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
691+ var templ_7745c5c3_Var33 string
692+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
660693 if templ_7745c5c3_Err != nil {
661- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 15}
694+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 161, Col: 15}
662695 }
663- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
696+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
664697 if templ_7745c5c3_Err != nil {
665698 return templ_7745c5c3_Err
666699 }
667- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<time datetime=\"")
700+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<time datetime=\"")
668701 if templ_7745c5c3_Err != nil {
669702 return templ_7745c5c3_Err
670703 }
671- var templ_7745c5c3_Var33 string
672- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt)
704+ var templ_7745c5c3_Var34 string
705+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt)
673706 if templ_7745c5c3_Err != nil {
674- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 49}
707+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 161, Col: 49}
675708 }
676- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
709+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
677710 if templ_7745c5c3_Err != nil {
678711 return templ_7745c5c3_Err
679712 }
680- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" title=\"")
713+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\" title=\"")
681714 if templ_7745c5c3_Err != nil {
682715 return templ_7745c5c3_Err
683716 }
684- var templ_7745c5c3_Var34 string
685- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs)
717+ var templ_7745c5c3_Var35 string
718+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs)
686719 if templ_7745c5c3_Err != nil {
687- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 72}
720+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 161, Col: 72}
688721 }
689- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
722+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
690723 if templ_7745c5c3_Err != nil {
691724 return templ_7745c5c3_Err
692725 }
693- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">")
726+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\">")
694727 if templ_7745c5c3_Err != nil {
695728 return templ_7745c5c3_Err
696729 }
697- var templ_7745c5c3_Var35 string
698- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When)
730+ var templ_7745c5c3_Var36 string
731+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When)
699732 if templ_7745c5c3_Err != nil {
700- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 90}
733+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 161, Col: 90}
701734 }
702- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
735+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
703736 if templ_7745c5c3_Err != nil {
704737 return templ_7745c5c3_Err
705738 }
706- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</time>")
739+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</time>")
707740 if templ_7745c5c3_Err != nil {
708741 return templ_7745c5c3_Err
709742 }
710743 }
711- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</dd>")
744+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</dd>")
712745 if templ_7745c5c3_Err != nil {
713746 return templ_7745c5c3_Err
714747 }
715- if p.ClosedBy != nil {
716- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<dt>closed by</dt><dd><a class=\"ref\" href=\"")
748+ if p.ResolvedBy != nil {
749+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<dt>resolved by</dt><dd><a class=\"ref\" href=\"")
717750 if templ_7745c5c3_Err != nil {
718751 return templ_7745c5c3_Err
719752 }
720- var templ_7745c5c3_Var36 templ.SafeURL
721- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.ClosedBy.Href))
753+ var templ_7745c5c3_Var37 templ.SafeURL
754+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.ResolvedBy.Href))
722755 if templ_7745c5c3_Err != nil {
723- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 154, Col: 62}
756+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 166, Col: 64}
724757 }
725- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
758+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
726759 if templ_7745c5c3_Err != nil {
727760 return templ_7745c5c3_Err
728761 }
729- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\">")
762+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">")
730763 if templ_7745c5c3_Err != nil {
731764 return templ_7745c5c3_Err
732765 }
733- var templ_7745c5c3_Var37 string
734- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(p.ClosedBy.Label)
766+ var templ_7745c5c3_Var38 string
767+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(p.ResolvedBy.Label)
735768 if templ_7745c5c3_Err != nil {
736- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 154, Col: 83}
769+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 166, Col: 87}
737770 }
738- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
771+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
739772 if templ_7745c5c3_Err != nil {
740773 return templ_7745c5c3_Err
741774 }
742- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</a></dd>")
775+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</a></dd>")
743776 if templ_7745c5c3_Err != nil {
744777 return templ_7745c5c3_Err
745778 }
746779 }
747780 if p.UpdatedAt != "" {
748- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<dt>updated</dt><dd><time datetime=\"")
781+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<dt>updated</dt><dd><time datetime=\"")
749782 if templ_7745c5c3_Err != nil {
750783 return templ_7745c5c3_Err
751784 }
752- var templ_7745c5c3_Var38 string
753- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt)
785+ var templ_7745c5c3_Var39 string
786+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt)
754787 if templ_7745c5c3_Err != nil {
755- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 158, Col: 38}
788+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 170, Col: 38}
756789 }
757- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
790+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
758791 if templ_7745c5c3_Err != nil {
759792 return templ_7745c5c3_Err
760793 }
761- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\">")
794+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">")
762795 if templ_7745c5c3_Err != nil {
763796 return templ_7745c5c3_Err
764797 }
765- var templ_7745c5c3_Var39 string
766- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated)
798+ var templ_7745c5c3_Var40 string
799+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated)
767800 if templ_7745c5c3_Err != nil {
768- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 158, Col: 52}
801+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 170, Col: 52}
769802 }
770- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
803+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
771804 if templ_7745c5c3_Err != nil {
772805 return templ_7745c5c3_Err
773806 }
774- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</time></dd>")
807+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</time></dd>")
775808 if templ_7745c5c3_Err != nil {
776809 return templ_7745c5c3_Err
777810 }
778811 }
779- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</dl></header>")
812+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</dl></header>")
780813 if templ_7745c5c3_Err != nil {
781814 return templ_7745c5c3_Err
782815 }
783816 if p.Desc != "" {
784- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>")
817+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>")
785818 if templ_7745c5c3_Err != nil {
786819 return templ_7745c5c3_Err
787820 }
@@ -789,12 +822,12 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
789822 if templ_7745c5c3_Err != nil {
790823 return templ_7745c5c3_Err
791824 }
792- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</article>")
825+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</article>")
793826 if templ_7745c5c3_Err != nil {
794827 return templ_7745c5c3_Err
795828 }
796829 }
797- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</section>")
830+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</section>")
798831 if templ_7745c5c3_Err != nil {
799832 return templ_7745c5c3_Err
800833 }
@@ -804,40 +837,38 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
804837 return templ_7745c5c3_Err
805838 }
806839 }
807- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " <h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">")
840+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " <h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">")
808841 if templ_7745c5c3_Err != nil {
809842 return templ_7745c5c3_Err
810843 }
811- var templ_7745c5c3_Var40 string
812- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Comments)))
844+ var templ_7745c5c3_Var41 string
845+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Comments)))
813846 if templ_7745c5c3_Err != nil {
814- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 171, Col: 99}
847+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 183, Col: 99}
815848 }
816- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
849+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
817850 if templ_7745c5c3_Err != nil {
818851 return templ_7745c5c3_Err
819852 }
820- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</span></h2>")
853+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</span></h2>")
821854 if templ_7745c5c3_Err != nil {
822855 return templ_7745c5c3_Err
823856 }
824- for _, c := range p.Comments {
825- templ_7745c5c3_Err = commentView(c, "").Render(ctx, templ_7745c5c3_Buffer)
826- if templ_7745c5c3_Err != nil {
827- return templ_7745c5c3_Err
828- }
857+ templ_7745c5c3_Err = issueTimeline(p.Timeline).Render(ctx, templ_7745c5c3_Buffer)
858+ if templ_7745c5c3_Err != nil {
859+ return templ_7745c5c3_Err
829860 }
830- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " ")
861+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " ")
831862 if templ_7745c5c3_Err != nil {
832863 return templ_7745c5c3_Err
833864 }
834865 if len(p.Comments) == 0 {
835- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<p class=\"dim\">no comments yet.</p>")
866+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<p class=\"dim\">no comments yet.</p>")
836867 if templ_7745c5c3_Err != nil {
837868 return templ_7745c5c3_Err
838869 }
839870 }
840- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " ")
871+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " ")
841872 if templ_7745c5c3_Err != nil {
842873 return templ_7745c5c3_Err
843874 }
@@ -849,7 +880,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
849880 }
850881 return nil
851882 })
852- templ_7745c5c3_Err = Shell(l, issueRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var26), templ_7745c5c3_Buffer)
883+ templ_7745c5c3_Err = Shell(l, issueRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var27), templ_7745c5c3_Buffer)
853884 if templ_7745c5c3_Err != nil {
854885 return templ_7745c5c3_Err
855886 }
@@ -873,135 +904,135 @@ func issueRail(p IssuePage) templ.Component {
873904 }()
874905 }
875906 ctx = templ.InitializeContext(ctx)
876- templ_7745c5c3_Var41 := templ.GetChildren(ctx)
877- if templ_7745c5c3_Var41 == nil {
878- templ_7745c5c3_Var41 = templ.NopComponent
907+ templ_7745c5c3_Var42 := templ.GetChildren(ctx)
908+ if templ_7745c5c3_Var42 == nil {
909+ templ_7745c5c3_Var42 = templ.NopComponent
879910 }
880911 ctx = templ.ClearChildren(ctx)
881912 if len(p.References) > 0 {
882- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Merge requests</h2><ul>")
913+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Merge requests</h2><ul>")
883914 if templ_7745c5c3_Err != nil {
884915 return templ_7745c5c3_Err
885916 }
886917 for _, r := range p.References {
887- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<li><a class=\"ref\" href=\"")
918+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<li><a class=\"ref\" href=\"")
888919 if templ_7745c5c3_Err != nil {
889920 return templ_7745c5c3_Err
890921 }
891- var templ_7745c5c3_Var42 templ.SafeURL
892- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
922+ var templ_7745c5c3_Var43 templ.SafeURL
923+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
893924 if templ_7745c5c3_Err != nil {
894- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 191, Col: 49}
925+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 201, Col: 49}
895926 }
896- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
927+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
897928 if templ_7745c5c3_Err != nil {
898929 return templ_7745c5c3_Err
899930 }
900- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\">")
931+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\">")
901932 if templ_7745c5c3_Err != nil {
902933 return templ_7745c5c3_Err
903934 }
904- var templ_7745c5c3_Var43 string
905- templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number)
935+ var templ_7745c5c3_Var44 string
936+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number)
906937 if templ_7745c5c3_Err != nil {
907- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 191, Col: 62}
938+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 201, Col: 62}
908939 }
909- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
940+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
910941 if templ_7745c5c3_Err != nil {
911942 return templ_7745c5c3_Err
912943 }
913- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</a> ")
944+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</a> ")
914945 if templ_7745c5c3_Err != nil {
915946 return templ_7745c5c3_Err
916947 }
917- var templ_7745c5c3_Var44 string
918- templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(" " + r.Title + " ")
948+ var templ_7745c5c3_Var45 string
949+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(" " + r.Title + " ")
919950 if templ_7745c5c3_Err != nil {
920- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 192, Col: 27}
951+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 202, Col: 27}
921952 }
922- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
953+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
923954 if templ_7745c5c3_Err != nil {
924955 return templ_7745c5c3_Err
925956 }
926- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " <span class=\"mr-state\" data-state=\"")
957+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, " <span class=\"mr-state\" data-state=\"")
927958 if templ_7745c5c3_Err != nil {
928959 return templ_7745c5c3_Err
929960 }
930- var templ_7745c5c3_Var45 string
931- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.State)
961+ var templ_7745c5c3_Var46 string
962+ templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.State)
932963 if templ_7745c5c3_Err != nil {
933- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 193, Col: 49}
964+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 203, Col: 49}
934965 }
935- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var45)
966+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var46)
936967 if templ_7745c5c3_Err != nil {
937968 return templ_7745c5c3_Err
938969 }
939- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\">")
970+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\">")
940971 if templ_7745c5c3_Err != nil {
941972 return templ_7745c5c3_Err
942973 }
943- var templ_7745c5c3_Var46 string
944- templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
974+ var templ_7745c5c3_Var47 string
975+ templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
945976 if templ_7745c5c3_Err != nil {
946- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 193, Col: 61}
977+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 203, Col: 61}
947978 }
948- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
979+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
949980 if templ_7745c5c3_Err != nil {
950981 return templ_7745c5c3_Err
951982 }
952- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</span> ")
983+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</span> ")
953984 if templ_7745c5c3_Err != nil {
954985 return templ_7745c5c3_Err
955986 }
956987 if r.Closes {
957- var templ_7745c5c3_Var47 string
958- templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(" · closes")
988+ var templ_7745c5c3_Var48 string
989+ templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(" · resolves")
959990 if templ_7745c5c3_Err != nil {
960- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 195, Col: 21}
991+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 205, Col: 23}
961992 }
962- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
993+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
963994 if templ_7745c5c3_Err != nil {
964995 return templ_7745c5c3_Err
965996 }
966997 }
967- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</li>")
998+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</li>")
968999 if templ_7745c5c3_Err != nil {
9691000 return templ_7745c5c3_Err
9701001 }
9711002 }
972- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</ul></section>")
1003+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</ul></section>")
9731004 if templ_7745c5c3_Err != nil {
9741005 return templ_7745c5c3_Err
9751006 }
9761007 }
9771008 if p.Transition != nil {
978- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "<section aria-labelledby=\"r-act\"><h2 id=\"r-act\">Issue</h2><form method=\"post\" action=\"")
1009+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<section aria-labelledby=\"r-act\"><h2 id=\"r-act\">Issue</h2><form method=\"post\" action=\"")
9791010 if templ_7745c5c3_Err != nil {
9801011 return templ_7745c5c3_Err
9811012 }
982- var templ_7745c5c3_Var48 templ.SafeURL
983- templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Transition.Action))
1013+ var templ_7745c5c3_Var49 templ.SafeURL
1014+ templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Transition.Action))
9841015 if templ_7745c5c3_Err != nil {
985- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 205, Col: 66}
1016+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 215, Col: 66}
9861017 }
987- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
1018+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
9881019 if templ_7745c5c3_Err != nil {
9891020 return templ_7745c5c3_Err
9901021 }
991- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\"><button type=\"submit\" class=\"btn\">")
1022+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\"><button type=\"submit\" class=\"btn\">")
9921023 if templ_7745c5c3_Err != nil {
9931024 return templ_7745c5c3_Err
9941025 }
995- var templ_7745c5c3_Var49 string
996- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(p.Transition.Label)
1026+ var templ_7745c5c3_Var50 string
1027+ templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(p.Transition.Label)
9971028 if templ_7745c5c3_Err != nil {
998- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 206, Col: 58}
1029+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 216, Col: 58}
9991030 }
1000- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
1031+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
10011032 if templ_7745c5c3_Err != nil {
10021033 return templ_7745c5c3_Err
10031034 }
1004- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</button></form></section>")
1035+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</button></form></section>")
10051036 if templ_7745c5c3_Err != nil {
10061037 return templ_7745c5c3_Err
10071038 }
@@ -67,7 +67,7 @@ func IssueList(l Layout, p IssueListPage) templ.Component {
67 if templ_7745c5c3_Err != nil {67 if templ_7745c5c3_Err != nil {
68 return templ_7745c5c3_Err68 return templ_7745c5c3_Err
69 }69 }
70- if !p.Closed {70+ if !p.Closed && !p.Resolved {
71 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " aria-current=\"page\"")71 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " aria-current=\"page\"")
72 if templ_7745c5c3_Err != nil {72 if templ_7745c5c3_Err != nil {
73 return templ_7745c5c3_Err73 return templ_7745c5c3_Err
@@ -96,35 +96,63 @@ func IssueList(l Layout, p IssueListPage) templ.Component {
96 return templ_7745c5c3_Err96 return templ_7745c5c3_Err
97 }97 }
98 }98 }
99- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">closed</a></nav><div class=\"filter\"><label for=\"issue-filter\" class=\"sr\">filter</label> <input id=\"issue-filter\" type=\"search\" data-filter=\".mrs tbody tr\" placeholder=\"Filter by title, author or state…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><h2 class=\"rule-h\">")99+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">closed</a> <a href=\"")
100 if templ_7745c5c3_Err != nil {100 if templ_7745c5c3_Err != nil {
101 return templ_7745c5c3_Err101 return templ_7745c5c3_Err
102 }102 }
103- if p.Closed {103+ var templ_7745c5c3_Var5 templ.SafeURL
104- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span>Closed</span> ")104+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.ResolvedHref))
105+ if templ_7745c5c3_Err != nil {
106+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 25, Col: 40}
107+ }
108+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
109+ if templ_7745c5c3_Err != nil {
110+ return templ_7745c5c3_Err
111+ }
112+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"")
113+ if templ_7745c5c3_Err != nil {
114+ return templ_7745c5c3_Err
115+ }
116+ if p.Resolved {
117+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " aria-current=\"page\"")
118+ if templ_7745c5c3_Err != nil {
119+ return templ_7745c5c3_Err
120+ }
121+ }
122+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ">resolved</a></nav><div class=\"filter\"><label for=\"issue-filter\" class=\"sr\">filter</label> <input id=\"issue-filter\" type=\"search\" data-filter=\".mrs tbody tr\" placeholder=\"Filter by title, author or state…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><h2 class=\"rule-h\">")
123+ if templ_7745c5c3_Err != nil {
124+ return templ_7745c5c3_Err
125+ }
126+ if p.Resolved {
127+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span>Resolved</span> ")
128+ if templ_7745c5c3_Err != nil {
129+ return templ_7745c5c3_Err
130+ }
131+ } else if p.Closed {
132+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span>Closed</span> ")
105 if templ_7745c5c3_Err != nil {133 if templ_7745c5c3_Err != nil {
106 return templ_7745c5c3_Err134 return templ_7745c5c3_Err
107 }135 }
108 } else {136 } else {
109- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span>Open</span> ")137+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span>Open</span> ")
110 if templ_7745c5c3_Err != nil {138 if templ_7745c5c3_Err != nil {
111 return templ_7745c5c3_Err139 return templ_7745c5c3_Err
112 }140 }
113 }141 }
114- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"count\">")142+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span class=\"count\">")
115 if templ_7745c5c3_Err != nil {143 if templ_7745c5c3_Err != nil {
116 return templ_7745c5c3_Err144 return templ_7745c5c3_Err
117 }145 }
118- var templ_7745c5c3_Var5 string146+ var templ_7745c5c3_Var6 string
119- templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))147+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))
120 if templ_7745c5c3_Err != nil {148 if templ_7745c5c3_Err != nil {
121- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 42, Col: 42}149+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 50, Col: 42}
122 }150 }
123- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))151+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
124 if templ_7745c5c3_Err != nil {152 if templ_7745c5c3_Err != nil {
125 return templ_7745c5c3_Err153 return templ_7745c5c3_Err
126 }154 }
127- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></h2>")155+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></h2>")
128 if templ_7745c5c3_Err != nil {156 if templ_7745c5c3_Err != nil {
129 return templ_7745c5c3_Err157 return templ_7745c5c3_Err
130 }158 }
@@ -132,81 +160,86 @@ func IssueList(l Layout, p IssueListPage) templ.Component {
132 if templ_7745c5c3_Err != nil {160 if templ_7745c5c3_Err != nil {
133 return templ_7745c5c3_Err161 return templ_7745c5c3_Err
134 }162 }
135- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ")163+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " ")
136 if templ_7745c5c3_Err != nil {164 if templ_7745c5c3_Err != nil {
137 return templ_7745c5c3_Err165 return templ_7745c5c3_Err
138 }166 }
139 if len(p.Rows) == 0 {167 if len(p.Rows) == 0 {
140- if p.Closed {168+ if p.Resolved {
141- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<p class=\"lede dim\">no closed issues.</p>")169+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<p class=\"lede dim\">no resolved issues.</p>")
170+ if templ_7745c5c3_Err != nil {
171+ return templ_7745c5c3_Err
172+ }
173+ } else if p.Closed {
174+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<p class=\"lede dim\">no closed issues.</p>")
142 if templ_7745c5c3_Err != nil {175 if templ_7745c5c3_Err != nil {
143 return templ_7745c5c3_Err176 return templ_7745c5c3_Err
144 }177 }
145 } else {178 } else {
146- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<p class=\"lede dim\">no open issues.</p>")179+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<p class=\"lede dim\">no open issues.</p>")
147 if templ_7745c5c3_Err != nil {180 if templ_7745c5c3_Err != nil {
148 return templ_7745c5c3_Err181 return templ_7745c5c3_Err
149 }182 }
150 }183 }
151 }184 }
152- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ")185+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ")
153 if templ_7745c5c3_Err != nil {186 if templ_7745c5c3_Err != nil {
154 return templ_7745c5c3_Err187 return templ_7745c5c3_Err
155 }188 }
156 if p.Older != "" || p.Newest != "" {189 if p.Older != "" || p.Newest != "" {
157- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<nav class=\"pager\" aria-label=\"Pages\">")190+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<nav class=\"pager\" aria-label=\"Pages\">")
158 if templ_7745c5c3_Err != nil {191 if templ_7745c5c3_Err != nil {
159 return templ_7745c5c3_Err192 return templ_7745c5c3_Err
160 }193 }
161 if p.Newest != "" {194 if p.Newest != "" {
162- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<a class=\"btn\" href=\"")195+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<a class=\"btn\" href=\"")
163 if templ_7745c5c3_Err != nil {196 if templ_7745c5c3_Err != nil {
164 return templ_7745c5c3_Err197 return templ_7745c5c3_Err
165 }198 }
166- var templ_7745c5c3_Var6 templ.SafeURL199+ var templ_7745c5c3_Var7 templ.SafeURL
167- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Newest))200+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Newest))
168 if templ_7745c5c3_Err != nil {201 if templ_7745c5c3_Err != nil {
169- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 55, Col: 50}202+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 65, Col: 50}
170 }203 }
171- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))204+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
172 if templ_7745c5c3_Err != nil {205 if templ_7745c5c3_Err != nil {
173 return templ_7745c5c3_Err206 return templ_7745c5c3_Err
174 }207 }
175- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">← Newest</a> ")208+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">← Newest</a> ")
176 if templ_7745c5c3_Err != nil {209 if templ_7745c5c3_Err != nil {
177 return templ_7745c5c3_Err210 return templ_7745c5c3_Err
178 }211 }
179 } else {212 } else {
180- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"btn\" aria-disabled=\"true\">← Newest</span> ")213+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<span class=\"btn\" aria-disabled=\"true\">← Newest</span> ")
181 if templ_7745c5c3_Err != nil {214 if templ_7745c5c3_Err != nil {
182 return templ_7745c5c3_Err215 return templ_7745c5c3_Err
183 }216 }
184 }217 }
185 if p.Older != "" {218 if p.Older != "" {
186- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<a class=\"btn\" href=\"")219+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<a class=\"btn\" href=\"")
187 if templ_7745c5c3_Err != nil {220 if templ_7745c5c3_Err != nil {
188 return templ_7745c5c3_Err221 return templ_7745c5c3_Err
189 }222 }
190- var templ_7745c5c3_Var7 templ.SafeURL223+ var templ_7745c5c3_Var8 templ.SafeURL
191- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Older))224+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Older))
192 if templ_7745c5c3_Err != nil {225 if templ_7745c5c3_Err != nil {
193- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 60, Col: 49}226+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 70, Col: 49}
194 }227 }
195- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))228+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
196 if templ_7745c5c3_Err != nil {229 if templ_7745c5c3_Err != nil {
197 return templ_7745c5c3_Err230 return templ_7745c5c3_Err
198 }231 }
199- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" rel=\"next\">Older →</a>")232+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" rel=\"next\">Older →</a>")
200 if templ_7745c5c3_Err != nil {233 if templ_7745c5c3_Err != nil {
201 return templ_7745c5c3_Err234 return templ_7745c5c3_Err
202 }235 }
203 } else {236 } else {
204- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"btn\" aria-disabled=\"true\">Older →</span>")237+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<span class=\"btn\" aria-disabled=\"true\">Older →</span>")
205 if templ_7745c5c3_Err != nil {238 if templ_7745c5c3_Err != nil {
206 return templ_7745c5c3_Err239 return templ_7745c5c3_Err
207 }240 }
208 }241 }
209- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</nav>")242+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</nav>")
210 if templ_7745c5c3_Err != nil {243 if templ_7745c5c3_Err != nil {
211 return templ_7745c5c3_Err244 return templ_7745c5c3_Err
212 }245 }
@@ -237,160 +270,160 @@ func issueRows(rows []IssueRow) templ.Component {
237 }()270 }()
238 }271 }
239 ctx = templ.InitializeContext(ctx)272 ctx = templ.InitializeContext(ctx)
240- templ_7745c5c3_Var8 := templ.GetChildren(ctx)273+ templ_7745c5c3_Var9 := templ.GetChildren(ctx)
241- if templ_7745c5c3_Var8 == nil {274+ if templ_7745c5c3_Var9 == nil {
242- templ_7745c5c3_Var8 = templ.NopComponent275+ templ_7745c5c3_Var9 = templ.NopComponent
243 }276 }
244 ctx = templ.ClearChildren(ctx)277 ctx = templ.ClearChildren(ctx)
245 if len(rows) > 0 {278 if len(rows) > 0 {
246- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<table class=\"list mrs\" aria-label=\"Issues\"><tbody data-rows>")279+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<table class=\"list mrs\" aria-label=\"Issues\"><tbody data-rows>")
247 if templ_7745c5c3_Err != nil {280 if templ_7745c5c3_Err != nil {
248 return templ_7745c5c3_Err281 return templ_7745c5c3_Err
249 }282 }
250 for _, i := range rows {283 for _, i := range rows {
251- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<tr")284+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<tr")
252 if templ_7745c5c3_Err != nil {285 if templ_7745c5c3_Err != nil {
253 return templ_7745c5c3_Err286 return templ_7745c5c3_Err
254 }287 }
255 if i.Cursor {288 if i.Cursor {
256- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " data-cursor")289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " data-cursor")
257 if templ_7745c5c3_Err != nil {290 if templ_7745c5c3_Err != nil {
258 return templ_7745c5c3_Err291 return templ_7745c5c3_Err
259 }292 }
260 }293 }
261- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " data-state=\"")294+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " data-state=\"")
262 if templ_7745c5c3_Err != nil {295 if templ_7745c5c3_Err != nil {
263 return templ_7745c5c3_Err296 return templ_7745c5c3_Err
264 }297 }
265- var templ_7745c5c3_Var9 string298+ var templ_7745c5c3_Var10 string
266- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.State)299+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.State)
267 if templ_7745c5c3_Err != nil {300 if templ_7745c5c3_Err != nil {
268- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 74, Col: 55}301+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 84, Col: 55}
269 }302 }
270- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)303+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
271 if templ_7745c5c3_Err != nil {304 if templ_7745c5c3_Err != nil {
272 return templ_7745c5c3_Err305 return templ_7745c5c3_Err
273 }306 }
274- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\"><td class=\"c-n mute\">")307+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"><td class=\"c-n mute\">")
275 if templ_7745c5c3_Err != nil {308 if templ_7745c5c3_Err != nil {
276 return templ_7745c5c3_Err309 return templ_7745c5c3_Err
277 }310 }
278- var templ_7745c5c3_Var10 string311+ var templ_7745c5c3_Var11 string
279- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(i.Number)312+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(i.Number)
280 if templ_7745c5c3_Err != nil {313 if templ_7745c5c3_Err != nil {
281- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 75, Col: 37}314+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 85, Col: 37}
282 }315 }
283- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))316+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
284 if templ_7745c5c3_Err != nil {317 if templ_7745c5c3_Err != nil {
285 return templ_7745c5c3_Err318 return templ_7745c5c3_Err
286 }319 }
287- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td><td class=\"c-state\">")320+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</td><td class=\"c-state\">")
288 if templ_7745c5c3_Err != nil {321 if templ_7745c5c3_Err != nil {
289 return templ_7745c5c3_Err322 return templ_7745c5c3_Err
290 }323 }
291- var templ_7745c5c3_Var11 string324+ var templ_7745c5c3_Var12 string
292- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(i.State)325+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(i.State)
293 if templ_7745c5c3_Err != nil {326 if templ_7745c5c3_Err != nil {
294- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 76, Col: 35}327+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 86, Col: 35}
295 }328 }
296- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))329+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
297 if templ_7745c5c3_Err != nil {330 if templ_7745c5c3_Err != nil {
298 return templ_7745c5c3_Err331 return templ_7745c5c3_Err
299 }332 }
300- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td><td class=\"c-subject\"><a href=\"")333+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</td><td class=\"c-subject\"><a href=\"")
301 if templ_7745c5c3_Err != nil {334 if templ_7745c5c3_Err != nil {
302 return templ_7745c5c3_Err335 return templ_7745c5c3_Err
303 }336 }
304- var templ_7745c5c3_Var12 templ.SafeURL337+ var templ_7745c5c3_Var13 templ.SafeURL
305- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(i.Href))338+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(i.Href))
306 if templ_7745c5c3_Err != nil {339 if templ_7745c5c3_Err != nil {
307- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 77, Col: 59}340+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 87, Col: 59}
308 }341 }
309- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))342+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
310 if templ_7745c5c3_Err != nil {343 if templ_7745c5c3_Err != nil {
311 return templ_7745c5c3_Err344 return templ_7745c5c3_Err
312 }345 }
313- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">")346+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">")
314 if templ_7745c5c3_Err != nil {347 if templ_7745c5c3_Err != nil {
315 return templ_7745c5c3_Err348 return templ_7745c5c3_Err
316 }349 }
317- var templ_7745c5c3_Var13 string350+ var templ_7745c5c3_Var14 string
318- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(i.Title)351+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(i.Title)
319 if templ_7745c5c3_Err != nil {352 if templ_7745c5c3_Err != nil {
320- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 77, Col: 71}353+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 87, Col: 71}
321 }354 }
322- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))355+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
323 if templ_7745c5c3_Err != nil {356 if templ_7745c5c3_Err != nil {
324 return templ_7745c5c3_Err357 return templ_7745c5c3_Err
325 }358 }
326- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</a></td><td class=\"c-who dim\">")359+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</a></td><td class=\"c-who dim\">")
327 if templ_7745c5c3_Err != nil {360 if templ_7745c5c3_Err != nil {
328 return templ_7745c5c3_Err361 return templ_7745c5c3_Err
329 }362 }
330- var templ_7745c5c3_Var14 string363+ var templ_7745c5c3_Var15 string
331- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(i.Author)364+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(i.Author)
332 if templ_7745c5c3_Err != nil {365 if templ_7745c5c3_Err != nil {
333- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 78, Col: 38}366+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 88, Col: 38}
334 }367 }
335- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))368+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
336 if templ_7745c5c3_Err != nil {369 if templ_7745c5c3_Err != nil {
337 return templ_7745c5c3_Err370 return templ_7745c5c3_Err
338 }371 }
339- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</td><td class=\"c-when nowrap dim\">")372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</td><td class=\"c-when nowrap dim\">")
340 if templ_7745c5c3_Err != nil {373 if templ_7745c5c3_Err != nil {
341 return templ_7745c5c3_Err374 return templ_7745c5c3_Err
342 }375 }
343 if i.WhenAt != "" {376 if i.WhenAt != "" {
344- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<time datetime=\"")377+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<time datetime=\"")
345 if templ_7745c5c3_Err != nil {378 if templ_7745c5c3_Err != nil {
346 return templ_7745c5c3_Err379 return templ_7745c5c3_Err
347 }380 }
348- var templ_7745c5c3_Var15 string381+ var templ_7745c5c3_Var16 string
349- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.WhenAt)382+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.WhenAt)
350 if templ_7745c5c3_Err != nil {383 if templ_7745c5c3_Err != nil {
351- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 81, Col: 33}384+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 91, Col: 33}
352 }385 }
353- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)386+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
354 if templ_7745c5c3_Err != nil {387 if templ_7745c5c3_Err != nil {
355 return templ_7745c5c3_Err388 return templ_7745c5c3_Err
356 }389 }
357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" title=\"")390+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" title=\"")
358 if templ_7745c5c3_Err != nil {391 if templ_7745c5c3_Err != nil {
359 return templ_7745c5c3_Err392 return templ_7745c5c3_Err
360 }393 }
361- var templ_7745c5c3_Var16 string394+ var templ_7745c5c3_Var17 string
362- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.WhenAt)395+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(i.WhenAt)
363 if templ_7745c5c3_Err != nil {396 if templ_7745c5c3_Err != nil {
364- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 81, Col: 52}397+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 91, Col: 52}
365 }398 }
366- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)399+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
367 if templ_7745c5c3_Err != nil {400 if templ_7745c5c3_Err != nil {
368 return templ_7745c5c3_Err401 return templ_7745c5c3_Err
369 }402 }
370- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">")403+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">")
371 if templ_7745c5c3_Err != nil {404 if templ_7745c5c3_Err != nil {
372 return templ_7745c5c3_Err405 return templ_7745c5c3_Err
373 }406 }
374- var templ_7745c5c3_Var17 string407+ var templ_7745c5c3_Var18 string
375- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(i.When)408+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(i.When)
376 if templ_7745c5c3_Err != nil {409 if templ_7745c5c3_Err != nil {
377- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 81, Col: 63}410+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 91, Col: 63}
378 }411 }
379- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))412+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
380 if templ_7745c5c3_Err != nil {413 if templ_7745c5c3_Err != nil {
381 return templ_7745c5c3_Err414 return templ_7745c5c3_Err
382 }415 }
383- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</time>")416+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</time>")
384 if templ_7745c5c3_Err != nil {417 if templ_7745c5c3_Err != nil {
385 return templ_7745c5c3_Err418 return templ_7745c5c3_Err
386 }419 }
387 }420 }
388- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</td></tr>")421+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</td></tr>")
389 if templ_7745c5c3_Err != nil {422 if templ_7745c5c3_Err != nil {
390 return templ_7745c5c3_Err423 return templ_7745c5c3_Err
391 }424 }
392 }425 }
393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</tbody></table>")426+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</tbody></table>")
394 if templ_7745c5c3_Err != nil {427 if templ_7745c5c3_Err != nil {
395 return templ_7745c5c3_Err428 return templ_7745c5c3_Err
396 }429 }
@@ -415,30 +448,30 @@ func issueListRail(p IssueListPage) templ.Component {
415 }()448 }()
416 }449 }
417 ctx = templ.InitializeContext(ctx)450 ctx = templ.InitializeContext(ctx)
418- templ_7745c5c3_Var18 := templ.GetChildren(ctx)451+ templ_7745c5c3_Var19 := templ.GetChildren(ctx)
419- if templ_7745c5c3_Var18 == nil {452+ if templ_7745c5c3_Var19 == nil {
420- templ_7745c5c3_Var18 = templ.NopComponent453+ templ_7745c5c3_Var19 = templ.NopComponent
421 }454 }
422 ctx = templ.ClearChildren(ctx)455 ctx = templ.ClearChildren(ctx)
423- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<section aria-labelledby=\"r-states\"><h2 id=\"r-states\">States</h2><dl class=\"kv\"><dt>open</dt><dd>being worked on</dd><dt>closed</dt><dd>done with, can be reopened</dd></dl></section>")456+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<section aria-labelledby=\"r-states\"><h2 id=\"r-states\">States</h2><dl class=\"kv\"><dt>open</dt><dd>being worked on</dd><dt>closed</dt><dd>closed without resolution</dd><dt>resolved</dt><dd>fixed, can be reopened</dd></dl></section>")
424 if templ_7745c5c3_Err != nil {457 if templ_7745c5c3_Err != nil {
425 return templ_7745c5c3_Err458 return templ_7745c5c3_Err
426 }459 }
427 if p.NewHref != "" {460 if p.NewHref != "" {
428- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<section aria-labelledby=\"r-new\"><h2 id=\"r-new\">Open one</h2><a class=\"btn primary\" href=\"")461+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<section aria-labelledby=\"r-new\"><h2 id=\"r-new\">Open one</h2><a class=\"btn primary\" href=\"")
429 if templ_7745c5c3_Err != nil {462 if templ_7745c5c3_Err != nil {
430 return templ_7745c5c3_Err463 return templ_7745c5c3_Err
431 }464 }
432- var templ_7745c5c3_Var19 templ.SafeURL465+ var templ_7745c5c3_Var20 templ.SafeURL
433- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.NewHref))466+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.NewHref))
434 if templ_7745c5c3_Err != nil {467 if templ_7745c5c3_Err != nil {
435- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 104, Col: 57}468+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 116, Col: 57}
436 }469 }
437- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))470+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
438 if templ_7745c5c3_Err != nil {471 if templ_7745c5c3_Err != nil {
439 return templ_7745c5c3_Err472 return templ_7745c5c3_Err
440 }473 }
441- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\">New issue</a></section>")474+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">New issue</a></section>")
442 if templ_7745c5c3_Err != nil {475 if templ_7745c5c3_Err != nil {
443 return templ_7745c5c3_Err476 return templ_7745c5c3_Err
444 }477 }
@@ -464,12 +497,12 @@ func IssueNew(l Layout, p IssueNewPage) templ.Component {
464 }()497 }()
465 }498 }
466 ctx = templ.InitializeContext(ctx)499 ctx = templ.InitializeContext(ctx)
467- templ_7745c5c3_Var20 := templ.GetChildren(ctx)500+ templ_7745c5c3_Var21 := templ.GetChildren(ctx)
468- if templ_7745c5c3_Var20 == nil {501+ if templ_7745c5c3_Var21 == nil {
469- templ_7745c5c3_Var20 = templ.NopComponent502+ templ_7745c5c3_Var21 = templ.NopComponent
470 }503 }
471 ctx = templ.ClearChildren(ctx)504 ctx = templ.ClearChildren(ctx)
472- templ_7745c5c3_Var21 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {505+ templ_7745c5c3_Var22 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
473 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context506 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
474 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)507 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
475 if !templ_7745c5c3_IsBuffer {508 if !templ_7745c5c3_IsBuffer {
@@ -481,39 +514,39 @@ func IssueNew(l Layout, p IssueNewPage) templ.Component {
481 }()514 }()
482 }515 }
483 ctx = templ.InitializeContext(ctx)516 ctx = templ.InitializeContext(ctx)
484- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<header class=\"show mr-h\"><h1>New issue</h1></header><form class=\"cform create\" method=\"post\" action=\"")517+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<header class=\"show mr-h\"><h1>New issue</h1></header><form class=\"cform create\" method=\"post\" action=\"")
485 if templ_7745c5c3_Err != nil {518 if templ_7745c5c3_Err != nil {
486 return templ_7745c5c3_Err519 return templ_7745c5c3_Err
487 }520 }
488- var templ_7745c5c3_Var22 templ.SafeURL521+ var templ_7745c5c3_Var23 templ.SafeURL
489- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))522+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))
490 if templ_7745c5c3_Err != nil {523 if templ_7745c5c3_Err != nil {
491- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 115, Col: 75}524+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 127, Col: 75}
492 }525 }
493- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))526+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
494 if templ_7745c5c3_Err != nil {527 if templ_7745c5c3_Err != nil {
495 return templ_7745c5c3_Err528 return templ_7745c5c3_Err
496 }529 }
497- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"><label for=\"issue-title\">title</label> <input id=\"issue-title\" name=\"title\" type=\"text\" maxlength=\"120\" required autocomplete=\"off\" autofocus> <label for=\"issue-body\">description</label> <textarea id=\"issue-body\" name=\"body\" rows=\"8\" maxlength=\"10000\" placeholder=\"Markdown, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create issue</button> <a class=\"btn\" href=\"")530+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\"><label for=\"issue-title\">title</label> <input id=\"issue-title\" name=\"title\" type=\"text\" maxlength=\"120\" required autocomplete=\"off\" autofocus> <label for=\"issue-body\">description</label> <textarea id=\"issue-body\" name=\"body\" rows=\"8\" maxlength=\"10000\" placeholder=\"Markdown, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create issue</button> <a class=\"btn\" href=\"")
498 if templ_7745c5c3_Err != nil {531 if templ_7745c5c3_Err != nil {
499 return templ_7745c5c3_Err532 return templ_7745c5c3_Err
500 }533 }
501- var templ_7745c5c3_Var23 templ.SafeURL534+ var templ_7745c5c3_Var24 templ.SafeURL
502- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Cancel))535+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Cancel))
503 if templ_7745c5c3_Err != nil {536 if templ_7745c5c3_Err != nil {
504- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 122, Col: 49}537+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 134, Col: 49}
505 }538 }
506- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))539+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
507 if templ_7745c5c3_Err != nil {540 if templ_7745c5c3_Err != nil {
508 return templ_7745c5c3_Err541 return templ_7745c5c3_Err
509 }542 }
510- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">Cancel</a></div></form>")543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\">Cancel</a></div></form>")
511 if templ_7745c5c3_Err != nil {544 if templ_7745c5c3_Err != nil {
512 return templ_7745c5c3_Err545 return templ_7745c5c3_Err
513 }546 }
514 return nil547 return nil
515 })548 })
516- templ_7745c5c3_Err = Shell(l, issueNewRail()).Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer)549+ templ_7745c5c3_Err = Shell(l, issueNewRail()).Render(templ.WithChildren(ctx, templ_7745c5c3_Var22), templ_7745c5c3_Buffer)
517 if templ_7745c5c3_Err != nil {550 if templ_7745c5c3_Err != nil {
518 return templ_7745c5c3_Err551 return templ_7745c5c3_Err
519 }552 }
@@ -537,12 +570,12 @@ func issueNewRail() templ.Component {
537 }()570 }()
538 }571 }
539 ctx = templ.InitializeContext(ctx)572 ctx = templ.InitializeContext(ctx)
540- templ_7745c5c3_Var24 := templ.GetChildren(ctx)573+ templ_7745c5c3_Var25 := templ.GetChildren(ctx)
541- if templ_7745c5c3_Var24 == nil {574+ if templ_7745c5c3_Var25 == nil {
542- templ_7745c5c3_Var24 = templ.NopComponent575+ templ_7745c5c3_Var25 = templ.NopComponent
543 }576 }
544 ctx = templ.ClearChildren(ctx)577 ctx = templ.ClearChildren(ctx)
545- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<section aria-labelledby=\"r-about\"><h2 id=\"r-about\">Issues</h2><p class=\"dim\">issues share their numbers with the merge requests of the repository.</p></section>")578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<section aria-labelledby=\"r-about\"><h2 id=\"r-about\">Issues</h2><p class=\"dim\">issues share their numbers with the merge requests of the repository.</p></section>")
546 if templ_7745c5c3_Err != nil {579 if templ_7745c5c3_Err != nil {
547 return templ_7745c5c3_Err580 return templ_7745c5c3_Err
548 }581 }
@@ -568,12 +601,12 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
568 }()601 }()
569 }602 }
570 ctx = templ.InitializeContext(ctx)603 ctx = templ.InitializeContext(ctx)
571- templ_7745c5c3_Var25 := templ.GetChildren(ctx)604+ templ_7745c5c3_Var26 := templ.GetChildren(ctx)
572- if templ_7745c5c3_Var25 == nil {605+ if templ_7745c5c3_Var26 == nil {
573- templ_7745c5c3_Var25 = templ.NopComponent606+ templ_7745c5c3_Var26 = templ.NopComponent
574 }607 }
575 ctx = templ.ClearChildren(ctx)608 ctx = templ.ClearChildren(ctx)
576- templ_7745c5c3_Var26 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {609+ templ_7745c5c3_Var27 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
577 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context610 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
578 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)611 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
579 if !templ_7745c5c3_IsBuffer {612 if !templ_7745c5c3_IsBuffer {
@@ -585,203 +618,203 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
585 }()618 }()
586 }619 }
587 ctx = templ.InitializeContext(ctx)620 ctx = templ.InitializeContext(ctx)
588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1><span class=\"mute\">")621+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1><span class=\"mute\">")
589 if templ_7745c5c3_Err != nil {622 if templ_7745c5c3_Err != nil {
590 return templ_7745c5c3_Err623 return templ_7745c5c3_Err
591 }624 }
592- var templ_7745c5c3_Var27 string625+ var templ_7745c5c3_Var28 string
593- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)626+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)
594 if templ_7745c5c3_Err != nil {627 if templ_7745c5c3_Err != nil {
595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 141, Col: 37}628+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 153, Col: 37}
596 }629 }
597- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))630+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
598 if templ_7745c5c3_Err != nil {631 if templ_7745c5c3_Err != nil {
599 return templ_7745c5c3_Err632 return templ_7745c5c3_Err
600 }633 }
601- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span> ")634+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span> ")
602 if templ_7745c5c3_Err != nil {635 if templ_7745c5c3_Err != nil {
603 return templ_7745c5c3_Err636 return templ_7745c5c3_Err
604 }637 }
605- var templ_7745c5c3_Var28 string638+ var templ_7745c5c3_Var29 string
606- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)639+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
607 if templ_7745c5c3_Err != nil {640 if templ_7745c5c3_Err != nil {
608- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 141, Col: 56}641+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 153, Col: 56}
609 }642 }
610- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))643+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
611 if templ_7745c5c3_Err != nil {644 if templ_7745c5c3_Err != nil {
612 return templ_7745c5c3_Err645 return templ_7745c5c3_Err
613 }646 }
614- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</h1><dl class=\"kv show-kv\"><dt>state</dt><dd class=\"mr-state\" data-state=\"")647+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</h1><dl class=\"kv show-kv\"><dt>state</dt><dd class=\"mr-state\" data-state=\"")
615 if templ_7745c5c3_Err != nil {648 if templ_7745c5c3_Err != nil {
616 return templ_7745c5c3_Err649 return templ_7745c5c3_Err
617 }650 }
618- var templ_7745c5c3_Var29 string651+ var templ_7745c5c3_Var30 string
619- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State)652+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State)
620 if templ_7745c5c3_Err != nil {653 if templ_7745c5c3_Err != nil {
621- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 144, Col: 46}654+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 156, Col: 46}
622 }655 }
623- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)656+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
624 if templ_7745c5c3_Err != nil {657 if templ_7745c5c3_Err != nil {
625 return templ_7745c5c3_Err658 return templ_7745c5c3_Err
626 }659 }
627- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\">")660+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">")
628 if templ_7745c5c3_Err != nil {661 if templ_7745c5c3_Err != nil {
629 return templ_7745c5c3_Err662 return templ_7745c5c3_Err
630 }663 }
631- var templ_7745c5c3_Var30 string664+ var templ_7745c5c3_Var31 string
632- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)665+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)
633 if templ_7745c5c3_Err != nil {666 if templ_7745c5c3_Err != nil {
634- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 144, Col: 58}667+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 156, Col: 58}
635 }668 }
636- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))669+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
637 if templ_7745c5c3_Err != nil {670 if templ_7745c5c3_Err != nil {
638 return templ_7745c5c3_Err671 return templ_7745c5c3_Err
639 }672 }
640- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</dd><dt>opened</dt><dd>")673+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</dd><dt>opened</dt><dd>")
641 if templ_7745c5c3_Err != nil {674 if templ_7745c5c3_Err != nil {
642 return templ_7745c5c3_Err675 return templ_7745c5c3_Err
643 }676 }
644- var templ_7745c5c3_Var31 string677+ var templ_7745c5c3_Var32 string
645- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name)678+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name)
646 if templ_7745c5c3_Err != nil {679 if templ_7745c5c3_Err != nil {
647- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 147, Col: 21}680+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 159, Col: 21}
648 }681 }
649- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))682+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
650 if templ_7745c5c3_Err != nil {683 if templ_7745c5c3_Err != nil {
651 return templ_7745c5c3_Err684 return templ_7745c5c3_Err
652 }685 }
653- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " ")686+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " ")
654 if templ_7745c5c3_Err != nil {687 if templ_7745c5c3_Err != nil {
655 return templ_7745c5c3_Err688 return templ_7745c5c3_Err
656 }689 }
657 if p.Opened.WhenAt != "" {690 if p.Opened.WhenAt != "" {
658- var templ_7745c5c3_Var32 string691+ var templ_7745c5c3_Var33 string
659- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")692+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
660 if templ_7745c5c3_Err != nil {693 if templ_7745c5c3_Err != nil {
661- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 15}694+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 161, Col: 15}
662 }695 }
663- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))696+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
664 if templ_7745c5c3_Err != nil {697 if templ_7745c5c3_Err != nil {
665 return templ_7745c5c3_Err698 return templ_7745c5c3_Err
666 }699 }
667- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<time datetime=\"")700+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<time datetime=\"")
668 if templ_7745c5c3_Err != nil {701 if templ_7745c5c3_Err != nil {
669 return templ_7745c5c3_Err702 return templ_7745c5c3_Err
670 }703 }
671- var templ_7745c5c3_Var33 string704+ var templ_7745c5c3_Var34 string
672- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt)705+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt)
673 if templ_7745c5c3_Err != nil {706 if templ_7745c5c3_Err != nil {
674- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 49}707+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 161, Col: 49}
675 }708 }
676- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)709+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
677 if templ_7745c5c3_Err != nil {710 if templ_7745c5c3_Err != nil {
678 return templ_7745c5c3_Err711 return templ_7745c5c3_Err
679 }712 }
680- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" title=\"")713+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\" title=\"")
681 if templ_7745c5c3_Err != nil {714 if templ_7745c5c3_Err != nil {
682 return templ_7745c5c3_Err715 return templ_7745c5c3_Err
683 }716 }
684- var templ_7745c5c3_Var34 string717+ var templ_7745c5c3_Var35 string
685- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs)718+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs)
686 if templ_7745c5c3_Err != nil {719 if templ_7745c5c3_Err != nil {
687- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 72}720+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 161, Col: 72}
688 }721 }
689- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)722+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
690 if templ_7745c5c3_Err != nil {723 if templ_7745c5c3_Err != nil {
691 return templ_7745c5c3_Err724 return templ_7745c5c3_Err
692 }725 }
693- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">")726+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\">")
694 if templ_7745c5c3_Err != nil {727 if templ_7745c5c3_Err != nil {
695 return templ_7745c5c3_Err728 return templ_7745c5c3_Err
696 }729 }
697- var templ_7745c5c3_Var35 string730+ var templ_7745c5c3_Var36 string
698- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When)731+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When)
699 if templ_7745c5c3_Err != nil {732 if templ_7745c5c3_Err != nil {
700- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 149, Col: 90}733+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 161, Col: 90}
701 }734 }
702- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))735+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
703 if templ_7745c5c3_Err != nil {736 if templ_7745c5c3_Err != nil {
704 return templ_7745c5c3_Err737 return templ_7745c5c3_Err
705 }738 }
706- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</time>")739+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</time>")
707 if templ_7745c5c3_Err != nil {740 if templ_7745c5c3_Err != nil {
708 return templ_7745c5c3_Err741 return templ_7745c5c3_Err
709 }742 }
710 }743 }
711- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</dd>")744+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</dd>")
712 if templ_7745c5c3_Err != nil {745 if templ_7745c5c3_Err != nil {
713 return templ_7745c5c3_Err746 return templ_7745c5c3_Err
714 }747 }
715- if p.ClosedBy != nil {748+ if p.ResolvedBy != nil {
716- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<dt>closed by</dt><dd><a class=\"ref\" href=\"")749+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<dt>resolved by</dt><dd><a class=\"ref\" href=\"")
717 if templ_7745c5c3_Err != nil {750 if templ_7745c5c3_Err != nil {
718 return templ_7745c5c3_Err751 return templ_7745c5c3_Err
719 }752 }
720- var templ_7745c5c3_Var36 templ.SafeURL753+ var templ_7745c5c3_Var37 templ.SafeURL
721- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.ClosedBy.Href))754+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.ResolvedBy.Href))
722 if templ_7745c5c3_Err != nil {755 if templ_7745c5c3_Err != nil {
723- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 154, Col: 62}756+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 166, Col: 64}
724 }757 }
725- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))758+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
726 if templ_7745c5c3_Err != nil {759 if templ_7745c5c3_Err != nil {
727 return templ_7745c5c3_Err760 return templ_7745c5c3_Err
728 }761 }
729- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\">")762+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">")
730 if templ_7745c5c3_Err != nil {763 if templ_7745c5c3_Err != nil {
731 return templ_7745c5c3_Err764 return templ_7745c5c3_Err
732 }765 }
733- var templ_7745c5c3_Var37 string766+ var templ_7745c5c3_Var38 string
734- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(p.ClosedBy.Label)767+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(p.ResolvedBy.Label)
735 if templ_7745c5c3_Err != nil {768 if templ_7745c5c3_Err != nil {
736- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 154, Col: 83}769+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 166, Col: 87}
737 }770 }
738- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))771+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
739 if templ_7745c5c3_Err != nil {772 if templ_7745c5c3_Err != nil {
740 return templ_7745c5c3_Err773 return templ_7745c5c3_Err
741 }774 }
742- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</a></dd>")775+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</a></dd>")
743 if templ_7745c5c3_Err != nil {776 if templ_7745c5c3_Err != nil {
744 return templ_7745c5c3_Err777 return templ_7745c5c3_Err
745 }778 }
746 }779 }
747 if p.UpdatedAt != "" {780 if p.UpdatedAt != "" {
748- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<dt>updated</dt><dd><time datetime=\"")781+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<dt>updated</dt><dd><time datetime=\"")
749 if templ_7745c5c3_Err != nil {782 if templ_7745c5c3_Err != nil {
750 return templ_7745c5c3_Err783 return templ_7745c5c3_Err
751 }784 }
752- var templ_7745c5c3_Var38 string785+ var templ_7745c5c3_Var39 string
753- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt)786+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt)
754 if templ_7745c5c3_Err != nil {787 if templ_7745c5c3_Err != nil {
755- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 158, Col: 38}788+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 170, Col: 38}
756 }789 }
757- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)790+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
758 if templ_7745c5c3_Err != nil {791 if templ_7745c5c3_Err != nil {
759 return templ_7745c5c3_Err792 return templ_7745c5c3_Err
760 }793 }
761- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\">")794+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">")
762 if templ_7745c5c3_Err != nil {795 if templ_7745c5c3_Err != nil {
763 return templ_7745c5c3_Err796 return templ_7745c5c3_Err
764 }797 }
765- var templ_7745c5c3_Var39 string798+ var templ_7745c5c3_Var40 string
766- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated)799+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated)
767 if templ_7745c5c3_Err != nil {800 if templ_7745c5c3_Err != nil {
768- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 158, Col: 52}801+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 170, Col: 52}
769 }802 }
770- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))803+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
771 if templ_7745c5c3_Err != nil {804 if templ_7745c5c3_Err != nil {
772 return templ_7745c5c3_Err805 return templ_7745c5c3_Err
773 }806 }
774- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</time></dd>")807+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</time></dd>")
775 if templ_7745c5c3_Err != nil {808 if templ_7745c5c3_Err != nil {
776 return templ_7745c5c3_Err809 return templ_7745c5c3_Err
777 }810 }
778 }811 }
779- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</dl></header>")812+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</dl></header>")
780 if templ_7745c5c3_Err != nil {813 if templ_7745c5c3_Err != nil {
781 return templ_7745c5c3_Err814 return templ_7745c5c3_Err
782 }815 }
783 if p.Desc != "" {816 if p.Desc != "" {
784- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>")817+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>")
785 if templ_7745c5c3_Err != nil {818 if templ_7745c5c3_Err != nil {
786 return templ_7745c5c3_Err819 return templ_7745c5c3_Err
787 }820 }
@@ -789,12 +822,12 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
789 if templ_7745c5c3_Err != nil {822 if templ_7745c5c3_Err != nil {
790 return templ_7745c5c3_Err823 return templ_7745c5c3_Err
791 }824 }
792- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</article>")825+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</article>")
793 if templ_7745c5c3_Err != nil {826 if templ_7745c5c3_Err != nil {
794 return templ_7745c5c3_Err827 return templ_7745c5c3_Err
795 }828 }
796 }829 }
797- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</section>")830+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</section>")
798 if templ_7745c5c3_Err != nil {831 if templ_7745c5c3_Err != nil {
799 return templ_7745c5c3_Err832 return templ_7745c5c3_Err
800 }833 }
@@ -804,40 +837,38 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
804 return templ_7745c5c3_Err837 return templ_7745c5c3_Err
805 }838 }
806 }839 }
807- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " <h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">")840+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " <h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">")
808 if templ_7745c5c3_Err != nil {841 if templ_7745c5c3_Err != nil {
809 return templ_7745c5c3_Err842 return templ_7745c5c3_Err
810 }843 }
811- var templ_7745c5c3_Var40 string844+ var templ_7745c5c3_Var41 string
812- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Comments)))845+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Comments)))
813 if templ_7745c5c3_Err != nil {846 if templ_7745c5c3_Err != nil {
814- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 171, Col: 99}847+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 183, Col: 99}
815 }848 }
816- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))849+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
817 if templ_7745c5c3_Err != nil {850 if templ_7745c5c3_Err != nil {
818 return templ_7745c5c3_Err851 return templ_7745c5c3_Err
819 }852 }
820- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</span></h2>")853+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</span></h2>")
821 if templ_7745c5c3_Err != nil {854 if templ_7745c5c3_Err != nil {
822 return templ_7745c5c3_Err855 return templ_7745c5c3_Err
823 }856 }
824- for _, c := range p.Comments {857+ templ_7745c5c3_Err = issueTimeline(p.Timeline).Render(ctx, templ_7745c5c3_Buffer)
825- templ_7745c5c3_Err = commentView(c, "").Render(ctx, templ_7745c5c3_Buffer)858+ if templ_7745c5c3_Err != nil {
826- if templ_7745c5c3_Err != nil {859+ return templ_7745c5c3_Err
827- return templ_7745c5c3_Err
828- }
829 }860 }
830- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " ")861+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " ")
831 if templ_7745c5c3_Err != nil {862 if templ_7745c5c3_Err != nil {
832 return templ_7745c5c3_Err863 return templ_7745c5c3_Err
833 }864 }
834 if len(p.Comments) == 0 {865 if len(p.Comments) == 0 {
835- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<p class=\"dim\">no comments yet.</p>")866+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<p class=\"dim\">no comments yet.</p>")
836 if templ_7745c5c3_Err != nil {867 if templ_7745c5c3_Err != nil {
837 return templ_7745c5c3_Err868 return templ_7745c5c3_Err
838 }869 }
839 }870 }
840- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " ")871+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " ")
841 if templ_7745c5c3_Err != nil {872 if templ_7745c5c3_Err != nil {
842 return templ_7745c5c3_Err873 return templ_7745c5c3_Err
843 }874 }
@@ -849,7 +880,7 @@ func IssueShow(l Layout, p IssuePage) templ.Component {
849 }880 }
850 return nil881 return nil
851 })882 })
852- templ_7745c5c3_Err = Shell(l, issueRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var26), templ_7745c5c3_Buffer)883+ templ_7745c5c3_Err = Shell(l, issueRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var27), templ_7745c5c3_Buffer)
853 if templ_7745c5c3_Err != nil {884 if templ_7745c5c3_Err != nil {
854 return templ_7745c5c3_Err885 return templ_7745c5c3_Err
855 }886 }
@@ -873,135 +904,135 @@ func issueRail(p IssuePage) templ.Component {
873 }()904 }()
874 }905 }
875 ctx = templ.InitializeContext(ctx)906 ctx = templ.InitializeContext(ctx)
876- templ_7745c5c3_Var41 := templ.GetChildren(ctx)907+ templ_7745c5c3_Var42 := templ.GetChildren(ctx)
877- if templ_7745c5c3_Var41 == nil {908+ if templ_7745c5c3_Var42 == nil {
878- templ_7745c5c3_Var41 = templ.NopComponent909+ templ_7745c5c3_Var42 = templ.NopComponent
879 }910 }
880 ctx = templ.ClearChildren(ctx)911 ctx = templ.ClearChildren(ctx)
881 if len(p.References) > 0 {912 if len(p.References) > 0 {
882- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Merge requests</h2><ul>")913+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Merge requests</h2><ul>")
883 if templ_7745c5c3_Err != nil {914 if templ_7745c5c3_Err != nil {
884 return templ_7745c5c3_Err915 return templ_7745c5c3_Err
885 }916 }
886 for _, r := range p.References {917 for _, r := range p.References {
887- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<li><a class=\"ref\" href=\"")918+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<li><a class=\"ref\" href=\"")
888 if templ_7745c5c3_Err != nil {919 if templ_7745c5c3_Err != nil {
889 return templ_7745c5c3_Err920 return templ_7745c5c3_Err
890 }921 }
891- var templ_7745c5c3_Var42 templ.SafeURL922+ var templ_7745c5c3_Var43 templ.SafeURL
892- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))923+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
893 if templ_7745c5c3_Err != nil {924 if templ_7745c5c3_Err != nil {
894- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 191, Col: 49}925+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 201, Col: 49}
895 }926 }
896- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))927+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
897 if templ_7745c5c3_Err != nil {928 if templ_7745c5c3_Err != nil {
898 return templ_7745c5c3_Err929 return templ_7745c5c3_Err
899 }930 }
900- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\">")931+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\">")
901 if templ_7745c5c3_Err != nil {932 if templ_7745c5c3_Err != nil {
902 return templ_7745c5c3_Err933 return templ_7745c5c3_Err
903 }934 }
904- var templ_7745c5c3_Var43 string935+ var templ_7745c5c3_Var44 string
905- templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number)936+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number)
906 if templ_7745c5c3_Err != nil {937 if templ_7745c5c3_Err != nil {
907- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 191, Col: 62}938+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 201, Col: 62}
908 }939 }
909- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))940+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
910 if templ_7745c5c3_Err != nil {941 if templ_7745c5c3_Err != nil {
911 return templ_7745c5c3_Err942 return templ_7745c5c3_Err
912 }943 }
913- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</a> ")944+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</a> ")
914 if templ_7745c5c3_Err != nil {945 if templ_7745c5c3_Err != nil {
915 return templ_7745c5c3_Err946 return templ_7745c5c3_Err
916 }947 }
917- var templ_7745c5c3_Var44 string948+ var templ_7745c5c3_Var45 string
918- templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(" " + r.Title + " ")949+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(" " + r.Title + " ")
919 if templ_7745c5c3_Err != nil {950 if templ_7745c5c3_Err != nil {
920- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 192, Col: 27}951+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 202, Col: 27}
921 }952 }
922- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))953+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
923 if templ_7745c5c3_Err != nil {954 if templ_7745c5c3_Err != nil {
924 return templ_7745c5c3_Err955 return templ_7745c5c3_Err
925 }956 }
926- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " <span class=\"mr-state\" data-state=\"")957+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, " <span class=\"mr-state\" data-state=\"")
927 if templ_7745c5c3_Err != nil {958 if templ_7745c5c3_Err != nil {
928 return templ_7745c5c3_Err959 return templ_7745c5c3_Err
929 }960 }
930- var templ_7745c5c3_Var45 string961+ var templ_7745c5c3_Var46 string
931- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.State)962+ templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.State)
932 if templ_7745c5c3_Err != nil {963 if templ_7745c5c3_Err != nil {
933- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 193, Col: 49}964+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 203, Col: 49}
934 }965 }
935- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var45)966+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var46)
936 if templ_7745c5c3_Err != nil {967 if templ_7745c5c3_Err != nil {
937 return templ_7745c5c3_Err968 return templ_7745c5c3_Err
938 }969 }
939- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\">")970+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\">")
940 if templ_7745c5c3_Err != nil {971 if templ_7745c5c3_Err != nil {
941 return templ_7745c5c3_Err972 return templ_7745c5c3_Err
942 }973 }
943- var templ_7745c5c3_Var46 string974+ var templ_7745c5c3_Var47 string
944- templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)975+ templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
945 if templ_7745c5c3_Err != nil {976 if templ_7745c5c3_Err != nil {
946- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 193, Col: 61}977+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 203, Col: 61}
947 }978 }
948- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))979+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
949 if templ_7745c5c3_Err != nil {980 if templ_7745c5c3_Err != nil {
950 return templ_7745c5c3_Err981 return templ_7745c5c3_Err
951 }982 }
952- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</span> ")983+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</span> ")
953 if templ_7745c5c3_Err != nil {984 if templ_7745c5c3_Err != nil {
954 return templ_7745c5c3_Err985 return templ_7745c5c3_Err
955 }986 }
956 if r.Closes {987 if r.Closes {
957- var templ_7745c5c3_Var47 string988+ var templ_7745c5c3_Var48 string
958- templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(" · closes")989+ templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(" · resolves")
959 if templ_7745c5c3_Err != nil {990 if templ_7745c5c3_Err != nil {
960- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 195, Col: 21}991+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 205, Col: 23}
961 }992 }
962- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))993+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
963 if templ_7745c5c3_Err != nil {994 if templ_7745c5c3_Err != nil {
964 return templ_7745c5c3_Err995 return templ_7745c5c3_Err
965 }996 }
966 }997 }
967- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</li>")998+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</li>")
968 if templ_7745c5c3_Err != nil {999 if templ_7745c5c3_Err != nil {
969 return templ_7745c5c3_Err1000 return templ_7745c5c3_Err
970 }1001 }
971 }1002 }
972- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</ul></section>")1003+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</ul></section>")
973 if templ_7745c5c3_Err != nil {1004 if templ_7745c5c3_Err != nil {
974 return templ_7745c5c3_Err1005 return templ_7745c5c3_Err
975 }1006 }
976 }1007 }
977 if p.Transition != nil {1008 if p.Transition != nil {
978- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "<section aria-labelledby=\"r-act\"><h2 id=\"r-act\">Issue</h2><form method=\"post\" action=\"")1009+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<section aria-labelledby=\"r-act\"><h2 id=\"r-act\">Issue</h2><form method=\"post\" action=\"")
979 if templ_7745c5c3_Err != nil {1010 if templ_7745c5c3_Err != nil {
980 return templ_7745c5c3_Err1011 return templ_7745c5c3_Err
981 }1012 }
982- var templ_7745c5c3_Var48 templ.SafeURL1013+ var templ_7745c5c3_Var49 templ.SafeURL
983- templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Transition.Action))1014+ templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Transition.Action))
984 if templ_7745c5c3_Err != nil {1015 if templ_7745c5c3_Err != nil {
985- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 205, Col: 66}1016+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 215, Col: 66}
986 }1017 }
987- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))1018+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
988 if templ_7745c5c3_Err != nil {1019 if templ_7745c5c3_Err != nil {
989 return templ_7745c5c3_Err1020 return templ_7745c5c3_Err
990 }1021 }
991- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\"><button type=\"submit\" class=\"btn\">")1022+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\"><button type=\"submit\" class=\"btn\">")
992 if templ_7745c5c3_Err != nil {1023 if templ_7745c5c3_Err != nil {
993 return templ_7745c5c3_Err1024 return templ_7745c5c3_Err
994 }1025 }
995- var templ_7745c5c3_Var49 string1026+ var templ_7745c5c3_Var50 string
996- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(p.Transition.Label)1027+ templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(p.Transition.Label)
997 if templ_7745c5c3_Err != nil {1028 if templ_7745c5c3_Err != nil {
998- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 206, Col: 58}1029+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/issues.templ`, Line: 216, Col: 58}
999 }1030 }
1000- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))1031+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
1001 if templ_7745c5c3_Err != nil {1032 if templ_7745c5c3_Err != nil {
1002 return templ_7745c5c3_Err1033 return templ_7745c5c3_Err
1003 }1034 }
1004- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</button></form></section>")1035+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</button></form></section>")
1005 if templ_7745c5c3_Err != nil {1036 if templ_7745c5c3_Err != nil {
1006 return templ_7745c5c3_Err1037 return templ_7745c5c3_Err
1007 }1038 }
M internal/adapters/web/issues_test.go +93 -4
@@ -304,6 +304,9 @@ func TestIssueLiveNotices(t *testing.T) {
304304 }{
305305 {domain.IssueOpened{Issue: issue}, "issues", "", "issues changed"},
306306 {domain.IssueClosed{Issue: issue}, "issues", "", "issues changed"},
307+ {domain.IssueResolved{Issue: issue}, "issues", "", "issues changed"},
308+ {domain.IssueResolved{Issue: issue}, "issue", "5", "this issue was resolved"},
309+ {domain.IssueResolved{Issue: issue}, "issue", "6", ""},
307310 {domain.IssueOpened{Issue: issue}, "issue", "5", "an issue was opened"},
308311 {domain.IssueClosed{Issue: issue}, "issue", "5", "this issue was closed"},
309312 {domain.IssueReopened{Issue: issue}, "issue", "5", "this issue was reopened"},
@@ -338,7 +341,7 @@ func TestIssueShowReferences(t *testing.T) {
338341 r := newIssueRig(t, domain.IssuesPublic, true)
339342 title, err := domain.ParseMRTitle("fix <b>it</b>")
340343 require.NoError(t, err)
341- issue := testIssue(t, 5, domain.IssueStateClosed, "bug", "see #7 and #8")
344+ issue := testIssue(t, 5, domain.IssueStateResolved, "bug", "see #7 and #8")
342345 issue.ClosedByMergeRequest = 2
343346 r.issues.EXPECT().Get(mock.Anything, mock.Anything, repoName(t, "proj"), int64(5)).Return(ports.IssueDetail{
344347 Issue: issue,
@@ -353,12 +356,12 @@ func TestIssueShowReferences(t *testing.T) {
353356 rec := r.do("GET", "/proj/issues/5", false, nil)
354357 require.Equal(t, 200, rec.Code, rec.Body.String())
355358 body := rec.Body.String()
356- assert.Contains(t, body, "closed by")
359+ assert.Contains(t, body, "resolved by")
357360 assert.Contains(t, body, `href="/proj/merge-requests/2"`)
358361 assert.Contains(t, body, ">!2<")
359362 assert.Contains(t, body, ">!3<")
360363 assert.Contains(t, body, `id="r-refs"`)
361- assert.Contains(t, body, "closes")
364+ assert.Contains(t, body, "resolves")
362365 assert.Contains(t, body, "fix &lt;b&gt;it&lt;/b&gt;")
363366 assert.Contains(t, body, `href="/proj/merge-requests/7"`, "#7 in the body")
364367 assert.Contains(t, body, `title="merge request 7"`)
@@ -373,5 +376,91 @@ func TestIssueShowWithoutReferences(t *testing.T) {
373376 }, nil)
374377 body := r.do("GET", "/proj/issues/5", false, nil).Body.String()
375378 assert.NotContains(t, body, "r-refs")
376- assert.NotContains(t, body, "closed by")
379+ assert.NotContains(t, body, "resolved by")
380+}
381+
382+func TestIssueResolvedList(t *testing.T) {
383+ t.Parallel()
384+ r := newIssueRig(t, domain.IssuesOwner, false)
385+ for _, token := range []string{"", "next"} {
386+ r.issues.EXPECT().List(mock.Anything, mock.Anything, repoName(t, "proj"), ports.ListIssuesParams{State: domain.IssueStateResolved, PageSize: pageSize, PageToken: token}).Return(ports.IssuePage{Issues: []domain.Issue{testIssue(t, 5, domain.IssueStateResolved, "fixed", "")}, NextPageToken: "next"}, nil)
387+ path := "/proj/issues?state=resolved"
388+ if token != "" {
389+ path += "&page=" + token
390+ }
391+ rec := r.do("GET", path, false, nil)
392+ require.Equal(t, 200, rec.Code)
393+ body := rec.Body.String()
394+ assert.Contains(t, body, `href="/proj/issues?state=resolved" aria-current="page"`)
395+ assert.Contains(t, body, `data-state="resolved"`)
396+ assert.Contains(t, body, `href="/proj/issues?page=next&amp;state=resolved"`)
397+ }
398+}
399+
400+func TestIssueResolvedTimeline(t *testing.T) {
401+ t.Parallel()
402+ r := newIssueRig(t, domain.IssuesOwner, false)
403+ at := time.Date(2026, 1, 2, 12, 0, 0, 123, time.UTC)
404+ issue := testIssue(t, 5, domain.IssueStateResolved, "fixed", "")
405+ issue.ClosedByMergeRequest = 12
406+ text, err := domain.ParseIssueBody("comment between events")
407+ require.NoError(t, err)
408+ title, err := domain.ParseMRTitle("Fix login")
409+ require.NoError(t, err)
410+ r.issues.EXPECT().Get(mock.Anything, mock.Anything, mock.Anything, int64(5)).Return(ports.IssueDetail{
411+ Issue: issue,
412+ Comments: []domain.IssueComment{{ID: 7, Author: "owner", Text: text, CreatedAt: at}},
413+ Events: []ports.IssueEventDetail{
414+ {Event: domain.IssueEvent{ID: 1, Kind: domain.IssueEventReferenced, MergeRequest: 12, Actor: "owner", CreatedAt: at.Add(-time.Second)}, MergeRequestTitle: title},
415+ {Event: domain.IssueEvent{ID: 2, Kind: domain.IssueEventResolved, MergeRequest: 12, Actor: "owner", CreatedAt: at}, MergeRequestTitle: title},
416+ },
417+ }, nil)
418+ for _, owner := range []bool{false, true} {
419+ rec := r.do("GET", "/proj/issues/5", owner, nil)
420+ require.Equal(t, 200, rec.Code)
421+ body := rec.Body.String()
422+ assert.Contains(t, body, `data-state="resolved"`)
423+ assert.Contains(t, body, "resolved by")
424+ assert.Contains(t, body, "!12 Fix login")
425+ assert.Contains(t, body, `datetime="2026-01-02T12:00:00Z"`)
426+ assert.Less(t, strings.Index(body, `id="e1"`), strings.Index(body, `id="c7"`))
427+ assert.Less(t, strings.Index(body, `id="c7"`), strings.Index(body, `id="e2"`), "comment precedes event on exact timestamp tie")
428+ assert.NotContains(t, body, `/5/resolve`)
429+ assert.NotContains(t, body, `/5/close`)
430+ if owner {
431+ assert.Contains(t, body, `action="/proj/issues/5/reopen"`)
432+ } else {
433+ assert.NotContains(t, body, `/5/reopen`)
434+ }
435+ }
436+}
437+
438+func TestIssueHistoryEventKinds(t *testing.T) {
439+ t.Parallel()
440+ s := &Server{cfg: Config{Now: time.Now}}
441+ for _, tc := range []struct {
442+ kind domain.IssueEventKind
443+ mr int64
444+ closes bool
445+ text string
446+ }{
447+ {domain.IssueEventReferenced, 12, false, "referenced this in"},
448+ {domain.IssueEventReferenced, 12, true, "will resolve this in"},
449+ {domain.IssueEventClosed, 0, false, "closed this"},
450+ {domain.IssueEventReopened, 0, false, "reopened this"},
451+ {domain.IssueEventResolved, 0, false, "resolved this"},
452+ {domain.IssueEventResolved, 12, false, "resolved this with"},
453+ } {
454+ items := s.issueTimeline("proj", nil, []ports.IssueEventDetail{{Event: domain.IssueEvent{ID: 1, Kind: tc.kind, MergeRequest: tc.mr, Closes: tc.closes, Actor: "owner", CreatedAt: time.Now()}}}, false)
455+ require.Len(t, items, 1)
456+ body := renderToString(t, issueTimeline(items))
457+ assert.Contains(t, body, "owner "+tc.text)
458+ assert.Contains(t, body, `data-kind="`+string(tc.kind)+`"`)
459+ assert.Contains(t, body, `<ol class="timeline" aria-label="Conversation history">`)
460+ if tc.mr != 0 {
461+ assert.Contains(t, body, `href="/proj/merge-requests/12">!12</a>`)
462+ } else {
463+ assert.NotContains(t, body, `/merge-requests/`)
464+ }
465+ }
377466 }
@@ -304,6 +304,9 @@ func TestIssueLiveNotices(t *testing.T) {
304 }{304 }{
305 {domain.IssueOpened{Issue: issue}, "issues", "", "issues changed"},305 {domain.IssueOpened{Issue: issue}, "issues", "", "issues changed"},
306 {domain.IssueClosed{Issue: issue}, "issues", "", "issues changed"},306 {domain.IssueClosed{Issue: issue}, "issues", "", "issues changed"},
307+ {domain.IssueResolved{Issue: issue}, "issues", "", "issues changed"},
308+ {domain.IssueResolved{Issue: issue}, "issue", "5", "this issue was resolved"},
309+ {domain.IssueResolved{Issue: issue}, "issue", "6", ""},
307 {domain.IssueOpened{Issue: issue}, "issue", "5", "an issue was opened"},310 {domain.IssueOpened{Issue: issue}, "issue", "5", "an issue was opened"},
308 {domain.IssueClosed{Issue: issue}, "issue", "5", "this issue was closed"},311 {domain.IssueClosed{Issue: issue}, "issue", "5", "this issue was closed"},
309 {domain.IssueReopened{Issue: issue}, "issue", "5", "this issue was reopened"},312 {domain.IssueReopened{Issue: issue}, "issue", "5", "this issue was reopened"},
@@ -338,7 +341,7 @@ func TestIssueShowReferences(t *testing.T) {
338 r := newIssueRig(t, domain.IssuesPublic, true)341 r := newIssueRig(t, domain.IssuesPublic, true)
339 title, err := domain.ParseMRTitle("fix <b>it</b>")342 title, err := domain.ParseMRTitle("fix <b>it</b>")
340 require.NoError(t, err)343 require.NoError(t, err)
341- issue := testIssue(t, 5, domain.IssueStateClosed, "bug", "see #7 and #8")344+ issue := testIssue(t, 5, domain.IssueStateResolved, "bug", "see #7 and #8")
342 issue.ClosedByMergeRequest = 2345 issue.ClosedByMergeRequest = 2
343 r.issues.EXPECT().Get(mock.Anything, mock.Anything, repoName(t, "proj"), int64(5)).Return(ports.IssueDetail{346 r.issues.EXPECT().Get(mock.Anything, mock.Anything, repoName(t, "proj"), int64(5)).Return(ports.IssueDetail{
344 Issue: issue,347 Issue: issue,
@@ -353,12 +356,12 @@ func TestIssueShowReferences(t *testing.T) {
353 rec := r.do("GET", "/proj/issues/5", false, nil)356 rec := r.do("GET", "/proj/issues/5", false, nil)
354 require.Equal(t, 200, rec.Code, rec.Body.String())357 require.Equal(t, 200, rec.Code, rec.Body.String())
355 body := rec.Body.String()358 body := rec.Body.String()
356- assert.Contains(t, body, "closed by")359+ assert.Contains(t, body, "resolved by")
357 assert.Contains(t, body, `href="/proj/merge-requests/2"`)360 assert.Contains(t, body, `href="/proj/merge-requests/2"`)
358 assert.Contains(t, body, ">!2<")361 assert.Contains(t, body, ">!2<")
359 assert.Contains(t, body, ">!3<")362 assert.Contains(t, body, ">!3<")
360 assert.Contains(t, body, `id="r-refs"`)363 assert.Contains(t, body, `id="r-refs"`)
361- assert.Contains(t, body, "closes")364+ assert.Contains(t, body, "resolves")
362 assert.Contains(t, body, "fix &lt;b&gt;it&lt;/b&gt;")365 assert.Contains(t, body, "fix &lt;b&gt;it&lt;/b&gt;")
363 assert.Contains(t, body, `href="/proj/merge-requests/7"`, "#7 in the body")366 assert.Contains(t, body, `href="/proj/merge-requests/7"`, "#7 in the body")
364 assert.Contains(t, body, `title="merge request 7"`)367 assert.Contains(t, body, `title="merge request 7"`)
@@ -373,5 +376,91 @@ func TestIssueShowWithoutReferences(t *testing.T) {
373 }, nil)376 }, nil)
374 body := r.do("GET", "/proj/issues/5", false, nil).Body.String()377 body := r.do("GET", "/proj/issues/5", false, nil).Body.String()
375 assert.NotContains(t, body, "r-refs")378 assert.NotContains(t, body, "r-refs")
376- assert.NotContains(t, body, "closed by")379+ assert.NotContains(t, body, "resolved by")
380+}
381+
382+func TestIssueResolvedList(t *testing.T) {
383+ t.Parallel()
384+ r := newIssueRig(t, domain.IssuesOwner, false)
385+ for _, token := range []string{"", "next"} {
386+ r.issues.EXPECT().List(mock.Anything, mock.Anything, repoName(t, "proj"), ports.ListIssuesParams{State: domain.IssueStateResolved, PageSize: pageSize, PageToken: token}).Return(ports.IssuePage{Issues: []domain.Issue{testIssue(t, 5, domain.IssueStateResolved, "fixed", "")}, NextPageToken: "next"}, nil)
387+ path := "/proj/issues?state=resolved"
388+ if token != "" {
389+ path += "&page=" + token
390+ }
391+ rec := r.do("GET", path, false, nil)
392+ require.Equal(t, 200, rec.Code)
393+ body := rec.Body.String()
394+ assert.Contains(t, body, `href="/proj/issues?state=resolved" aria-current="page"`)
395+ assert.Contains(t, body, `data-state="resolved"`)
396+ assert.Contains(t, body, `href="/proj/issues?page=next&amp;state=resolved"`)
397+ }
398+}
399+
400+func TestIssueResolvedTimeline(t *testing.T) {
401+ t.Parallel()
402+ r := newIssueRig(t, domain.IssuesOwner, false)
403+ at := time.Date(2026, 1, 2, 12, 0, 0, 123, time.UTC)
404+ issue := testIssue(t, 5, domain.IssueStateResolved, "fixed", "")
405+ issue.ClosedByMergeRequest = 12
406+ text, err := domain.ParseIssueBody("comment between events")
407+ require.NoError(t, err)
408+ title, err := domain.ParseMRTitle("Fix login")
409+ require.NoError(t, err)
410+ r.issues.EXPECT().Get(mock.Anything, mock.Anything, mock.Anything, int64(5)).Return(ports.IssueDetail{
411+ Issue: issue,
412+ Comments: []domain.IssueComment{{ID: 7, Author: "owner", Text: text, CreatedAt: at}},
413+ Events: []ports.IssueEventDetail{
414+ {Event: domain.IssueEvent{ID: 1, Kind: domain.IssueEventReferenced, MergeRequest: 12, Actor: "owner", CreatedAt: at.Add(-time.Second)}, MergeRequestTitle: title},
415+ {Event: domain.IssueEvent{ID: 2, Kind: domain.IssueEventResolved, MergeRequest: 12, Actor: "owner", CreatedAt: at}, MergeRequestTitle: title},
416+ },
417+ }, nil)
418+ for _, owner := range []bool{false, true} {
419+ rec := r.do("GET", "/proj/issues/5", owner, nil)
420+ require.Equal(t, 200, rec.Code)
421+ body := rec.Body.String()
422+ assert.Contains(t, body, `data-state="resolved"`)
423+ assert.Contains(t, body, "resolved by")
424+ assert.Contains(t, body, "!12 Fix login")
425+ assert.Contains(t, body, `datetime="2026-01-02T12:00:00Z"`)
426+ assert.Less(t, strings.Index(body, `id="e1"`), strings.Index(body, `id="c7"`))
427+ assert.Less(t, strings.Index(body, `id="c7"`), strings.Index(body, `id="e2"`), "comment precedes event on exact timestamp tie")
428+ assert.NotContains(t, body, `/5/resolve`)
429+ assert.NotContains(t, body, `/5/close`)
430+ if owner {
431+ assert.Contains(t, body, `action="/proj/issues/5/reopen"`)
432+ } else {
433+ assert.NotContains(t, body, `/5/reopen`)
434+ }
435+ }
436+}
437+
438+func TestIssueHistoryEventKinds(t *testing.T) {
439+ t.Parallel()
440+ s := &Server{cfg: Config{Now: time.Now}}
441+ for _, tc := range []struct {
442+ kind domain.IssueEventKind
443+ mr int64
444+ closes bool
445+ text string
446+ }{
447+ {domain.IssueEventReferenced, 12, false, "referenced this in"},
448+ {domain.IssueEventReferenced, 12, true, "will resolve this in"},
449+ {domain.IssueEventClosed, 0, false, "closed this"},
450+ {domain.IssueEventReopened, 0, false, "reopened this"},
451+ {domain.IssueEventResolved, 0, false, "resolved this"},
452+ {domain.IssueEventResolved, 12, false, "resolved this with"},
453+ } {
454+ items := s.issueTimeline("proj", nil, []ports.IssueEventDetail{{Event: domain.IssueEvent{ID: 1, Kind: tc.kind, MergeRequest: tc.mr, Closes: tc.closes, Actor: "owner", CreatedAt: time.Now()}}}, false)
455+ require.Len(t, items, 1)
456+ body := renderToString(t, issueTimeline(items))
457+ assert.Contains(t, body, "owner "+tc.text)
458+ assert.Contains(t, body, `data-kind="`+string(tc.kind)+`"`)
459+ assert.Contains(t, body, `<ol class="timeline" aria-label="Conversation history">`)
460+ if tc.mr != 0 {
461+ assert.Contains(t, body, `href="/proj/merge-requests/12">!12</a>`)
462+ } else {
463+ assert.NotContains(t, body, `/merge-requests/`)
464+ }
465+ }
377 }466 }
M internal/adapters/web/mr.go +22 -1
@@ -99,6 +99,7 @@ type MRListPage struct {
9999
100100 // CommentView is a comment of a merge request, ready to show.
101101 type CommentView struct {
102+ createdAt time.Time
102103 ID string
103104 Author string
104105 When string
@@ -296,6 +297,8 @@ type MRPage struct {
296297 // loaded later) the loose comments of a listed file go under the file
297298 // instead (DiffFile.Loose).
298299 General, Loose []CommentView
300+ Timeline []TimelineItem
301+ Issues []IssueLinkView
299302 LooseInFiles bool
300303 NComments int
301304 NUnresolved int
@@ -634,6 +637,24 @@ func (s *Server) mrPageView(ctx context.Context, r *http.Request, since, fragmen
634637 on = parseOn(r.URL.Query().Get("on"))
635638 }
636639 s.placeComments(ctx, &p, d.name(), n, res.Comments, head, !since, canWrite, on)
640+ p.Timeline = s.issueTimeline(d.name(), p.General, res.IssueEvents, true)
641+ // Event DTOs carry MR titles, not issue titles. Enrich from current links
642+ // when available; removed references still retain a numbered history link.
643+ issueTitles := make(map[string]string, len(res.Issues))
644+ for _, issue := range res.Issues {
645+ issueTitles[issueHref(d.name(), issue.Number)] = visibleText(issue.Title.String())
646+ }
647+ for _, item := range p.Timeline {
648+ if item.Event != nil && item.Event.MR != nil {
649+ if title := issueTitles[item.Event.MR.Href]; title != "" {
650+ item.Event.MR.Label += " " + title
651+ }
652+ }
653+ }
654+ for _, issue := range res.Issues {
655+ p.Issues = append(p.Issues, IssueLinkView{Number: "#" + strconv.FormatInt(issue.Number, 10), Title: visibleText(issue.Title.String()), State: string(issue.State), Href: issueHref(d.name(), issue.Number), Closes: issue.Closes})
656+ }
657+
637658 lines, invisible := diffLines(p.Files)
638659 p.Invisible = invisible
639660 p.DiffView = newDiffView(r, lines, withFrom(self+"?view=unified", from), withFrom(self+"?view=split", from))
@@ -1010,7 +1031,7 @@ func (s *Server) placeComments(ctx context.Context, p *MRPage, repo string, n in
10101031 if err != nil {
10111032 bodySplit = ""
10121033 }
1013- v := CommentView{BodySplit: bodySplit, ID: cid, Author: s.who(c.Author), Body: body, Resolved: c.Resolved, Deleted: c.Deleted, Edited: !c.EditedAt.IsZero(), Verdict: c.Verdict.String(), Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head}
1034+ v := CommentView{createdAt: c.CreatedAt, BodySplit: bodySplit, ID: cid, Author: s.who(c.Author), Body: body, Resolved: c.Resolved, Deleted: c.Deleted, Edited: !c.EditedAt.IsZero(), Verdict: c.Verdict.String(), Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head}
10141035 v.When, v.WhenAt = whenOf(now, c.CreatedAt)
10151036 if !c.CreatedAt.IsZero() {
10161037 v.Abs = absolute(c.CreatedAt)
@@ -99,6 +99,7 @@ type MRListPage struct {
99 99
100 // CommentView is a comment of a merge request, ready to show.100 // CommentView is a comment of a merge request, ready to show.
101 type CommentView struct {101 type CommentView struct {
102+ createdAt time.Time
102 ID string103 ID string
103 Author string104 Author string
104 When string105 When string
@@ -296,6 +297,8 @@ type MRPage struct {
296 // loaded later) the loose comments of a listed file go under the file297 // loaded later) the loose comments of a listed file go under the file
297 // instead (DiffFile.Loose).298 // instead (DiffFile.Loose).
298 General, Loose []CommentView299 General, Loose []CommentView
300+ Timeline []TimelineItem
301+ Issues []IssueLinkView
299 LooseInFiles bool302 LooseInFiles bool
300 NComments int303 NComments int
301 NUnresolved int304 NUnresolved int
@@ -634,6 +637,24 @@ func (s *Server) mrPageView(ctx context.Context, r *http.Request, since, fragmen
634 on = parseOn(r.URL.Query().Get("on"))637 on = parseOn(r.URL.Query().Get("on"))
635 }638 }
636 s.placeComments(ctx, &p, d.name(), n, res.Comments, head, !since, canWrite, on)639 s.placeComments(ctx, &p, d.name(), n, res.Comments, head, !since, canWrite, on)
640+ p.Timeline = s.issueTimeline(d.name(), p.General, res.IssueEvents, true)
641+ // Event DTOs carry MR titles, not issue titles. Enrich from current links
642+ // when available; removed references still retain a numbered history link.
643+ issueTitles := make(map[string]string, len(res.Issues))
644+ for _, issue := range res.Issues {
645+ issueTitles[issueHref(d.name(), issue.Number)] = visibleText(issue.Title.String())
646+ }
647+ for _, item := range p.Timeline {
648+ if item.Event != nil && item.Event.MR != nil {
649+ if title := issueTitles[item.Event.MR.Href]; title != "" {
650+ item.Event.MR.Label += " " + title
651+ }
652+ }
653+ }
654+ for _, issue := range res.Issues {
655+ p.Issues = append(p.Issues, IssueLinkView{Number: "#" + strconv.FormatInt(issue.Number, 10), Title: visibleText(issue.Title.String()), State: string(issue.State), Href: issueHref(d.name(), issue.Number), Closes: issue.Closes})
656+ }
657+
637 lines, invisible := diffLines(p.Files)658 lines, invisible := diffLines(p.Files)
638 p.Invisible = invisible659 p.Invisible = invisible
639 p.DiffView = newDiffView(r, lines, withFrom(self+"?view=unified", from), withFrom(self+"?view=split", from))660 p.DiffView = newDiffView(r, lines, withFrom(self+"?view=unified", from), withFrom(self+"?view=split", from))
@@ -1010,7 +1031,7 @@ func (s *Server) placeComments(ctx context.Context, p *MRPage, repo string, n in
1010 if err != nil {1031 if err != nil {
1011 bodySplit = ""1032 bodySplit = ""
1012 }1033 }
1013- v := CommentView{BodySplit: bodySplit, ID: cid, Author: s.who(c.Author), Body: body, Resolved: c.Resolved, Deleted: c.Deleted, Edited: !c.EditedAt.IsZero(), Verdict: c.Verdict.String(), Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head}1034+ v := CommentView{createdAt: c.CreatedAt, BodySplit: bodySplit, ID: cid, Author: s.who(c.Author), Body: body, Resolved: c.Resolved, Deleted: c.Deleted, Edited: !c.EditedAt.IsZero(), Verdict: c.Verdict.String(), Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head}
1014 v.When, v.WhenAt = whenOf(now, c.CreatedAt)1035 v.When, v.WhenAt = whenOf(now, c.CreatedAt)
1015 if !c.CreatedAt.IsZero() {1036 if !c.CreatedAt.IsZero() {
1016 v.Abs = absolute(c.CreatedAt)1037 v.Abs = absolute(c.CreatedAt)
M internal/adapters/web/mr.templ +21 -7
@@ -214,9 +214,7 @@ templ MergeRequest(l Layout, p MRPage) {
214214 for _, c := range p.Loose {
215215 @commentView(c, "")
216216 }
217- for _, c := range p.General {
218- @commentView(c, "")
219- }
217+ @issueTimeline(p.Timeline)
220218 if p.NComments == 0 {
221219 <p class="dim">no comments yet.</p>
222220 }
@@ -267,7 +265,8 @@ templ commentView(c CommentView, suffix string) {
267265 }
268266 </p>
269267 if len(c.Parts) > 0 {
270- <p class="c-text">@userText(c.Parts)</p>
268+ <p class="c-text">@userText(c.Parts)
269+</p>
271270 } else if c.Deleted {
272271 if c.Verdict != "" {
273272 <p class="mute">text deleted</p>
@@ -428,6 +427,21 @@ templ mrFooter(p MRPage) {
428427 }
429428
430429 templ mrRail(p MRPage) {
430+ if len(p.Issues) > 0 {
431+ <section aria-labelledby="r-issues">
432+ <h2 id="r-issues">Issues</h2>
433+ <ul>
434+ for _, i := range p.Issues {
435+ <li>
436+ <a class="ref" href={ templ.SafeURL(i.Href) }>{ i.Number } { i.Title }</a>{ " " }<span class="mr-state" data-state={ i.State }>{ i.State }</span>
437+ if i.Closes {
438+ { " · resolves" }
439+ }
440+ </li>
441+ }
442+ </ul>
443+ </section>
444+ }
431445 <section aria-labelledby="r-files" class="r-files">
432446 <h2 id="r-files">
433447 Files <span class="count">{ itoa(len(p.Files)) }</span><kbd aria-hidden="true">l</kbd>
@@ -808,7 +822,7 @@ templ mrActions(p MRPage, fragment bool) {
808822
809823 // MRActionStatus remains visible even when the request detail cannot be read.
810824 templ MRActionStatus(message, href string) {
811- <div id="mr-actions" class="mr-acts" hx-swap-oob="true">
812- <p class="d-err" role="alert">{ message } <a href={ templ.SafeURL(href) }>reload</a></p>
813- </div>
825+ <div id="mr-actions" class="mr-acts" hx-swap-oob="true">
826+ <p class="d-err" role="alert">{ message } <a href={ templ.SafeURL(href) }>reload</a></p>
827+ </div>
814828 }
@@ -214,9 +214,7 @@ templ MergeRequest(l Layout, p MRPage) {
214 for _, c := range p.Loose {214 for _, c := range p.Loose {
215 @commentView(c, "")215 @commentView(c, "")
216 }216 }
217- for _, c := range p.General {217+ @issueTimeline(p.Timeline)
218- @commentView(c, "")
219- }
220 if p.NComments == 0 {218 if p.NComments == 0 {
221 <p class="dim">no comments yet.</p>219 <p class="dim">no comments yet.</p>
222 }220 }
@@ -267,7 +265,8 @@ templ commentView(c CommentView, suffix string) {
267 }265 }
268 </p>266 </p>
269 if len(c.Parts) > 0 {267 if len(c.Parts) > 0 {
270- <p class="c-text">@userText(c.Parts)</p>268+ <p class="c-text">@userText(c.Parts)
269+</p>
271 } else if c.Deleted {270 } else if c.Deleted {
272 if c.Verdict != "" {271 if c.Verdict != "" {
273 <p class="mute">text deleted</p>272 <p class="mute">text deleted</p>
@@ -428,6 +427,21 @@ templ mrFooter(p MRPage) {
428 }427 }
429 428
430 templ mrRail(p MRPage) {429 templ mrRail(p MRPage) {
430+ if len(p.Issues) > 0 {
431+ <section aria-labelledby="r-issues">
432+ <h2 id="r-issues">Issues</h2>
433+ <ul>
434+ for _, i := range p.Issues {
435+ <li>
436+ <a class="ref" href={ templ.SafeURL(i.Href) }>{ i.Number } { i.Title }</a>{ " " }<span class="mr-state" data-state={ i.State }>{ i.State }</span>
437+ if i.Closes {
438+ { " · resolves" }
439+ }
440+ </li>
441+ }
442+ </ul>
443+ </section>
444+ }
431 <section aria-labelledby="r-files" class="r-files">445 <section aria-labelledby="r-files" class="r-files">
432 <h2 id="r-files">446 <h2 id="r-files">
433 Files <span class="count">{ itoa(len(p.Files)) }</span><kbd aria-hidden="true">l</kbd>447 Files <span class="count">{ itoa(len(p.Files)) }</span><kbd aria-hidden="true">l</kbd>
@@ -808,7 +822,7 @@ templ mrActions(p MRPage, fragment bool) {
808 822
809 // MRActionStatus remains visible even when the request detail cannot be read.823 // MRActionStatus remains visible even when the request detail cannot be read.
810 templ MRActionStatus(message, href string) {824 templ MRActionStatus(message, href string) {
811- <div id="mr-actions" class="mr-acts" hx-swap-oob="true">825+ <div id="mr-actions" class="mr-acts" hx-swap-oob="true">
812- <p class="d-err" role="alert">{ message } <a href={ templ.SafeURL(href) }>reload</a></p>826+ <p class="d-err" role="alert">{ message } <a href={ templ.SafeURL(href) }>reload</a></p>
813- </div>827+ </div>
814 }828 }
M internal/adapters/web/mr_templ.go +725 -618 generated file: collapsed
M internal/adapters/web/mr_test.go +35 -0
@@ -332,3 +332,38 @@ func TestMergeIgnoreChecks(t *testing.T) {
332332 assert.Equal(t, mrHref("demo", 3), to)
333333 }
334334 }
335+
336+func TestMRLinkedIssuesAndHistory(t *testing.T) {
337+ t.Parallel()
338+ r := newIssueRig(t, domain.IssuesOwner, false)
339+ at := time.Date(2026, 1, 2, 12, 0, 0, 123, time.UTC)
340+ title, err := domain.ParseIssueTitle("Fix <login>")
341+ require.NoError(t, err)
342+ text, err := domain.ParsePlainText("between issue events")
343+ require.NoError(t, err)
344+ r.mrs.EXPECT().Get(mock.Anything, mock.Anything, repoName(t, "proj"), int64(12), mock.Anything).Return(ports.MergeRequestDetail{
345+ MergeRequest: domain.MergeRequest{Repo: repoName(t, "proj"), Number: 12, State: domain.MRStateMerged, Source: "feature", Target: "main"},
346+ Issues: []ports.MergeRequestIssue{{Number: 3, Title: title, State: domain.IssueStateResolved, Closes: true}, {Number: 4, Title: title, State: domain.IssueStateOpen}},
347+ Comments: []domain.Comment{{ID: 7, Author: "owner", Text: text, CreatedAt: at}},
348+ IssueEvents: []ports.IssueEventDetail{
349+ {Event: domain.IssueEvent{ID: 1, Issue: 3, Kind: domain.IssueEventReferenced, Actor: "owner", CreatedAt: at.Add(-time.Second)}},
350+ {Event: domain.IssueEvent{ID: 2, Issue: 3, Kind: domain.IssueEventReferenced, Closes: true, Actor: "owner", CreatedAt: at}},
351+ {Event: domain.IssueEvent{ID: 3, Issue: 3, Kind: domain.IssueEventResolved, Actor: "owner", CreatedAt: at.Add(time.Second)}},
352+ {Event: domain.IssueEvent{ID: 1, Issue: 9, Kind: domain.IssueEventReferenced, Actor: "owner", CreatedAt: at.Add(2 * time.Second)}},
353+ },
354+ }, nil)
355+ rec := r.do("GET", "/proj/merge-requests/12", false, nil)
356+ require.Equal(t, 200, rec.Code, rec.Body.String())
357+ body := rec.Body.String()
358+ assert.Contains(t, body, `id="r-issues">Issues`)
359+ assert.Contains(t, body, `href="/proj/issues/3">#3 Fix &lt;login&gt;</a>`)
360+ assert.Contains(t, body, `data-state="resolved"`)
361+ assert.Contains(t, body, " · resolves")
362+ assert.Contains(t, body, "owner referenced")
363+ assert.Contains(t, body, "owner will resolve")
364+ assert.Contains(t, body, "owner resolved")
365+ assert.Contains(t, body, `href="/proj/issues/9">#9</a>`, "removed links keep their history")
366+ assert.Less(t, strings.Index(body, `id="e3-1"`), strings.Index(body, `id="c7"`))
367+ assert.Less(t, strings.Index(body, `id="c7"`), strings.Index(body, `id="e3-2"`))
368+ assert.Less(t, strings.Index(body, `id="e3-2"`), strings.Index(body, `id="e3-3"`))
369+}
@@ -332,3 +332,38 @@ func TestMergeIgnoreChecks(t *testing.T) {
332 assert.Equal(t, mrHref("demo", 3), to)332 assert.Equal(t, mrHref("demo", 3), to)
333 }333 }
334 }334 }
335+
336+func TestMRLinkedIssuesAndHistory(t *testing.T) {
337+ t.Parallel()
338+ r := newIssueRig(t, domain.IssuesOwner, false)
339+ at := time.Date(2026, 1, 2, 12, 0, 0, 123, time.UTC)
340+ title, err := domain.ParseIssueTitle("Fix <login>")
341+ require.NoError(t, err)
342+ text, err := domain.ParsePlainText("between issue events")
343+ require.NoError(t, err)
344+ r.mrs.EXPECT().Get(mock.Anything, mock.Anything, repoName(t, "proj"), int64(12), mock.Anything).Return(ports.MergeRequestDetail{
345+ MergeRequest: domain.MergeRequest{Repo: repoName(t, "proj"), Number: 12, State: domain.MRStateMerged, Source: "feature", Target: "main"},
346+ Issues: []ports.MergeRequestIssue{{Number: 3, Title: title, State: domain.IssueStateResolved, Closes: true}, {Number: 4, Title: title, State: domain.IssueStateOpen}},
347+ Comments: []domain.Comment{{ID: 7, Author: "owner", Text: text, CreatedAt: at}},
348+ IssueEvents: []ports.IssueEventDetail{
349+ {Event: domain.IssueEvent{ID: 1, Issue: 3, Kind: domain.IssueEventReferenced, Actor: "owner", CreatedAt: at.Add(-time.Second)}},
350+ {Event: domain.IssueEvent{ID: 2, Issue: 3, Kind: domain.IssueEventReferenced, Closes: true, Actor: "owner", CreatedAt: at}},
351+ {Event: domain.IssueEvent{ID: 3, Issue: 3, Kind: domain.IssueEventResolved, Actor: "owner", CreatedAt: at.Add(time.Second)}},
352+ {Event: domain.IssueEvent{ID: 1, Issue: 9, Kind: domain.IssueEventReferenced, Actor: "owner", CreatedAt: at.Add(2 * time.Second)}},
353+ },
354+ }, nil)
355+ rec := r.do("GET", "/proj/merge-requests/12", false, nil)
356+ require.Equal(t, 200, rec.Code, rec.Body.String())
357+ body := rec.Body.String()
358+ assert.Contains(t, body, `id="r-issues">Issues`)
359+ assert.Contains(t, body, `href="/proj/issues/3">#3 Fix &lt;login&gt;</a>`)
360+ assert.Contains(t, body, `data-state="resolved"`)
361+ assert.Contains(t, body, " · resolves")
362+ assert.Contains(t, body, "owner referenced")
363+ assert.Contains(t, body, "owner will resolve")
364+ assert.Contains(t, body, "owner resolved")
365+ assert.Contains(t, body, `href="/proj/issues/9">#9</a>`, "removed links keep their history")
366+ assert.Less(t, strings.Index(body, `id="e3-1"`), strings.Index(body, `id="c7"`))
367+ assert.Less(t, strings.Index(body, `id="c7"`), strings.Index(body, `id="e3-2"`))
368+ assert.Less(t, strings.Index(body, `id="e3-2"`), strings.Index(body, `id="e3-3"`))
369+}
M internal/adapters/web/static/css/60-mr.css +16 -0
@@ -1,4 +1,12 @@
11 @layer components {
2+ .timeline { list-style: none; padding: 0; margin: 0; }
3+ .sys { margin-block: var(--s3); padding: var(--s2) var(--s3); border-left: 2px solid var(--border); color: var(--ink-dim); font-size: var(--fs-s); overflow-wrap: anywhere; }
4+ .sys-icon { margin-inline-end: var(--s2); }
5+ .sys[data-kind="resolved"] .sys-icon { color: var(--teal); }
6+ .sys[data-kind="closed"] .sys-icon { color: var(--red); }
7+ .mr-state[data-state="resolved"]::before { content: "✓ "; color: var(--teal); }
8+ .mrs tr[data-state="resolved"] .c-state::before { content: "✓ "; color: var(--teal); }
9+
210 /* merge requests: the review page. Hue only where it carries a verdict. */
311 .mr-h .show-kv { margin-top: 0; }
412 #merge-menu .htmx-indicator { display: none; }
@@ -281,6 +289,14 @@
281289 }
282290
283291 @layer components {
292+ .timeline { list-style: none; padding: 0; margin: 0; }
293+ .sys { margin-block: var(--s3); padding: var(--s2) var(--s3); border-left: 2px solid var(--border); color: var(--ink-dim); font-size: var(--fs-s); overflow-wrap: anywhere; }
294+ .sys-icon { margin-inline-end: var(--s2); }
295+ .sys[data-kind="resolved"] .sys-icon { color: var(--teal); }
296+ .sys[data-kind="closed"] .sys-icon { color: var(--red); }
297+ .mr-state[data-state="resolved"]::before { content: "✓ "; color: var(--teal); }
298+ .mrs tr[data-state="resolved"] .c-state::before { content: "✓ "; color: var(--teal); }
299+
284300 .c-body { white-space: normal; overflow-wrap: anywhere; }
285301 .c-body.prose > * { margin-block: .5em; }
286302 .c-body .ref { font-variant-numeric: tabular-nums; text-decoration: underline; }
@@ -1,4 +1,12 @@
1 @layer components {1 @layer components {
2+ .timeline { list-style: none; padding: 0; margin: 0; }
3+ .sys { margin-block: var(--s3); padding: var(--s2) var(--s3); border-left: 2px solid var(--border); color: var(--ink-dim); font-size: var(--fs-s); overflow-wrap: anywhere; }
4+ .sys-icon { margin-inline-end: var(--s2); }
5+ .sys[data-kind="resolved"] .sys-icon { color: var(--teal); }
6+ .sys[data-kind="closed"] .sys-icon { color: var(--red); }
7+ .mr-state[data-state="resolved"]::before { content: "✓ "; color: var(--teal); }
8+ .mrs tr[data-state="resolved"] .c-state::before { content: "✓ "; color: var(--teal); }
9+
2 /* merge requests: the review page. Hue only where it carries a verdict. */10 /* merge requests: the review page. Hue only where it carries a verdict. */
3 .mr-h .show-kv { margin-top: 0; }11 .mr-h .show-kv { margin-top: 0; }
4 #merge-menu .htmx-indicator { display: none; }12 #merge-menu .htmx-indicator { display: none; }
@@ -281,6 +289,14 @@
281 }289 }
282 290
283 @layer components {291 @layer components {
292+ .timeline { list-style: none; padding: 0; margin: 0; }
293+ .sys { margin-block: var(--s3); padding: var(--s2) var(--s3); border-left: 2px solid var(--border); color: var(--ink-dim); font-size: var(--fs-s); overflow-wrap: anywhere; }
294+ .sys-icon { margin-inline-end: var(--s2); }
295+ .sys[data-kind="resolved"] .sys-icon { color: var(--teal); }
296+ .sys[data-kind="closed"] .sys-icon { color: var(--red); }
297+ .mr-state[data-state="resolved"]::before { content: "✓ "; color: var(--teal); }
298+ .mrs tr[data-state="resolved"] .c-state::before { content: "✓ "; color: var(--teal); }
299+
284 .c-body { white-space: normal; overflow-wrap: anywhere; }300 .c-body { white-space: normal; overflow-wrap: anywhere; }
285 .c-body.prose > * { margin-block: .5em; }301 .c-body.prose > * { margin-block: .5em; }
286 .c-body .ref { font-variant-numeric: tabular-nums; text-decoration: underline; }302 .c-body .ref { font-variant-numeric: tabular-nums; text-decoration: underline; }

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 45

↑↓ moveenter openesc close