detached 5e16168

#30 feat(web): release card, latest release in About, g-sequence shortcuts

state
merged · 7c1886f · squash
branch
feat/release-view-ux → main
opened
bnema ·
updated
CommitSubjectAuthorDateSignatureStatus
5e16168fix(releases): resolve the latest release from one snapshot and keep the index cheap to maintainbriceGPG Verified
879e0cfperf(releases): index the latest release per repositorybriceGPG Verified
3b03a2etest(app): update web goldens for g sequence shortcutsbriceGPG Verified
eb30609fix(web): drop sequence aria-keyshortcuts and use fake timers in keyboard testsbriceGPG Verified
a8c4b10test(web): cover g sequences in the jstest suitebriceGPG Verified
c3ae830feat(web): navigate sections with g sequences and fix uppercase action keysbriceGPG Verified
e08572cfeat(web): show releases as one card with prose notes and the latest release in AboutbriceGPG Verified

Checks 4

73 files · +2058 -1284
4/4
M README.md +1 -0
@@ -139,6 +139,7 @@ Give the agent the [`skills/ermite`](skills/ermite/SKILL.md) skill so it knows t
139139 - [Issues](docs/issues.md)
140140 - [Commit signatures](docs/commit-signatures.md): SSH and OpenPGP badges, trusted keys
141141 - [Web sign-in](docs/web-sign-in.md): passkeys and one-time links
142+- [Keyboard shortcuts](docs/keyboard-shortcuts.md): web section navigation and page actions
142143 - [CI](docs/ci.md): runners, trust, concurrency, reusable workflows
143144 - [CI events](docs/ci-events.md)
144145 - [OIDC](docs/ci-oidc.md)
@@ -139,6 +139,7 @@ Give the agent the [`skills/ermite`](skills/ermite/SKILL.md) skill so it knows t
139 - [Issues](docs/issues.md)139 - [Issues](docs/issues.md)
140 - [Commit signatures](docs/commit-signatures.md): SSH and OpenPGP badges, trusted keys140 - [Commit signatures](docs/commit-signatures.md): SSH and OpenPGP badges, trusted keys
141 - [Web sign-in](docs/web-sign-in.md): passkeys and one-time links141 - [Web sign-in](docs/web-sign-in.md): passkeys and one-time links
142+- [Keyboard shortcuts](docs/keyboard-shortcuts.md): web section navigation and page actions
142 - [CI](docs/ci.md): runners, trust, concurrency, reusable workflows143 - [CI](docs/ci.md): runners, trust, concurrency, reusable workflows
143 - [CI events](docs/ci-events.md)144 - [CI events](docs/ci-events.md)
144 - [OIDC](docs/ci-oidc.md)145 - [OIDC](docs/ci-oidc.md)
M docs/issues.md +1 -1
@@ -50,7 +50,7 @@ Limits: the 100 most recent commits of the merge request, 16 KiB of each text, 1
5050
5151 The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.
5252
53-The **Running** section, after **Awaiting moderation**, lists queued and in-progress CI runs newest first, with links to each run, its repository, ref, and start time. The `N running` counter counts every repository; the repository filter also applies to this section. **Shift+I** (`I`) opens the inbox from any page for the signed-in owner, unless single-key shortcuts are off; lowercase `i` still opens the issues tab.
53+The **Running** section, after **Awaiting moderation**, lists queued and in-progress CI runs newest first, with links to each run, its repository, ref, and start time. The `N running` counter counts every repository; the repository filter also applies to this section. **Shift+I** (`I`) opens the inbox from any page for the signed-in owner, unless single-key shortcuts are off; `g i` opens the issues tab.
5454
5555 The page updates live: run status changes, merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
5656
@@ -50,7 +50,7 @@ Limits: the 100 most recent commits of the merge request, 16 KiB of each text, 1
50 50
51 The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.51 The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.
52 52
53-The **Running** section, after **Awaiting moderation**, lists queued and in-progress CI runs newest first, with links to each run, its repository, ref, and start time. The `N running` counter counts every repository; the repository filter also applies to this section. **Shift+I** (`I`) opens the inbox from any page for the signed-in owner, unless single-key shortcuts are off; lowercase `i` still opens the issues tab.53+The **Running** section, after **Awaiting moderation**, lists queued and in-progress CI runs newest first, with links to each run, its repository, ref, and start time. The `N running` counter counts every repository; the repository filter also applies to this section. **Shift+I** (`I`) opens the inbox from any page for the signed-in owner, unless single-key shortcuts are off; `g i` opens the issues tab.
54 54
55 The page updates live: run status changes, merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.55 The page updates live: run status changes, merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
56 56
A docs/keyboard-shortcuts.md +30 -0
@@ -0,0 +1,30 @@
1+# Web keyboard shortcuts
2+
3+Press `?` for the complete, page-specific list. Shortcuts do not run while typing
4+in a field. Section navigation uses two successive keys, not a chord:
5+
6+| Keys | Destination |
7+| --- | --- |
8+| `g s` | Summary |
9+| `g f` | Files |
10+| `g c` | Commits |
11+| `g b` | Branches & tags |
12+| `g m` | Merge requests |
13+| `g i` | Issues |
14+| `g r` | Releases |
15+| `g a` | Actions (when CI is enabled) |
16+| `g h` | Home |
17+| `g g` | Top / first row |
18+| `G` (Shift+g) | Bottom / last row |
19+
20+There is no `g t` alias, and `s`, `t`, `c`, `r`, `m`, and `i` alone do
21+not navigate sections. `b` still opens the branch/tag switcher; `f`, `:`,
22+and Ctrl+K (Cmd+K on macOS) open the command palette.
23+
24+On merge requests, `l` focuses the files side panel; `g f` always navigates
25+to the repository Files section. `h` returns to the content. Uppercase `M`
26+opens the merge menu, `R` marks the head read, `E` expands every diff file,
27+and `I` opens the signed-in owner's inbox.
28+
29+The help dialog can turn off character shortcuts, including `g` sequences
30+and shifted letters. Ctrl+K / Cmd+K and Escape remain available.
@@ -0,0 +1,30 @@
1+# Web keyboard shortcuts
2+
3+Press `?` for the complete, page-specific list. Shortcuts do not run while typing
4+in a field. Section navigation uses two successive keys, not a chord:
5+
6+| Keys | Destination |
7+| --- | --- |
8+| `g s` | Summary |
9+| `g f` | Files |
10+| `g c` | Commits |
11+| `g b` | Branches & tags |
12+| `g m` | Merge requests |
13+| `g i` | Issues |
14+| `g r` | Releases |
15+| `g a` | Actions (when CI is enabled) |
16+| `g h` | Home |
17+| `g g` | Top / first row |
18+| `G` (Shift+g) | Bottom / last row |
19+
20+There is no `g t` alias, and `s`, `t`, `c`, `r`, `m`, and `i` alone do
21+not navigate sections. `b` still opens the branch/tag switcher; `f`, `:`,
22+and Ctrl+K (Cmd+K on macOS) open the command palette.
23+
24+On merge requests, `l` focuses the files side panel; `g f` always navigates
25+to the repository Files section. `h` returns to the content. Uppercase `M`
26+opens the merge menu, `R` marks the head read, `E` expands every diff file,
27+and `I` opens the signed-in owner's inbox.
28+
29+The help dialog can turn off character shortcuts, including `g` sequences
30+and shifted letters. Ctrl+K / Cmd+K and Escape remain available.
M docs/merge-requests.md +1 -1
@@ -142,7 +142,7 @@ the diff opens the Checks panel and continues to update live. Commit totals use
142142 `N+` when the branch list is capped: the reader's existing count operation counts all reachable
143143 ancestors, not the source-minus-target range.
144144
145-The rail keeps Files (including its `g f` shortcut) and Verdicts. Merge settings
145+The rail keeps Files (including its `l` shortcut) and Verdicts. Merge settings
146146 are read-only information in the More (⋯) menu, available to every viewer.
147147 Signing-key information is also there when a merge box offers signed modes;
148148 merge actions remain restricted to their existing permissions.
@@ -142,7 +142,7 @@ the diff opens the Checks panel and continues to update live. Commit totals use
142 `N+` when the branch list is capped: the reader's existing count operation counts all reachable142 `N+` when the branch list is capped: the reader's existing count operation counts all reachable
143 ancestors, not the source-minus-target range.143 ancestors, not the source-minus-target range.
144 144
145-The rail keeps Files (including its `g f` shortcut) and Verdicts. Merge settings145+The rail keeps Files (including its `l` shortcut) and Verdicts. Merge settings
146 are read-only information in the More (⋯) menu, available to every viewer.146 are read-only information in the More (⋯) menu, available to every viewer.
147 Signing-key information is also there when a merge box offers signed modes;147 Signing-key information is also there when a merge box offers signed modes;
148 merge actions remain restricted to their existing permissions.148 merge actions remain restricted to their existing permissions.
M internal/adapters/kvstore/issues_test.go +1 -1
@@ -364,7 +364,7 @@ func TestSchemaNewerThanThisBuildStopsIssuesData(t *testing.T) {
364364 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))
365365 _, err := kvstore.Open(ctx, dir)
366366 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)
367- assert.Equal(t, uint32(6), uint32(kvstore.SchemaVersion))
367+ assert.Equal(t, uint32(7), uint32(kvstore.SchemaVersion))
368368 }
369369
370370 // A reopen racing the delete of the repository either lands before the delete
@@ -364,7 +364,7 @@ func TestSchemaNewerThanThisBuildStopsIssuesData(t *testing.T) {
364 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))364 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))
365 _, err := kvstore.Open(ctx, dir)365 _, err := kvstore.Open(ctx, dir)
366 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)366 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)
367- assert.Equal(t, uint32(6), uint32(kvstore.SchemaVersion))367+ assert.Equal(t, uint32(7), uint32(kvstore.SchemaVersion))
368 }368 }
369 369
370 // A reopen racing the delete of the repository either lands before the delete370 // A reopen racing the delete of the repository either lands before the delete
M internal/adapters/kvstore/releases.go +28 -2
@@ -36,7 +36,26 @@ func relNamePrefix(repo domain.RepoName, id int64) string {
3636 func relNameKey(a domain.ReleaseAsset) []byte {
3737 return []byte(relNamePrefix(a.Repo, a.ReleaseID) + a.Name)
3838 }
39-func relUseKey(repo domain.RepoName) []byte { return []byte("reluse/" + repo.String()) }
39+func relLatestKey(repo domain.RepoName) []byte { return []byte("rellatest/" + repo.String()) }
40+func relUseKey(repo domain.RepoName) []byte { return []byte("reluse/" + repo.String()) }
41+
42+// GetLatestRelease resolves the index and release from the same snapshot.
43+func (c *Releases) GetLatestRelease(ctx context.Context, repo domain.RepoName) (domain.Release, error) {
44+ snap := c.db.Snapshot(ctx)
45+ item, ok := snap.Get(ctx, relLatestKey(repo))
46+ if !ok {
47+ return domain.Release{}, domain.ErrNotFound
48+ }
49+ id, err := strconv.ParseInt(string(item.Value), 10, 64)
50+ if err != nil {
51+ return domain.Release{}, err
52+ }
53+ item, ok = snap.Get(ctx, relKey(repo, id))
54+ if !ok {
55+ return domain.Release{}, domain.ErrNotFound
56+ }
57+ return decodeRelease(item)
58+}
4059 func encodeRelease(r domain.Release) []byte {
4160 return mustMarshal(&storagepb.Release{
4261 Id: r.ID,
@@ -285,6 +304,13 @@ func (c *Releases) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) erro
285304 b.RequireVersion(relUseKey(w.Repo), w.UsageVersion)
286305 }
287306 b.Put(relUseKey(w.Repo), mustMarshal(&storagepb.ReleaseUsage{Used: w.Used}))
307+ if w.SetLatest {
308+ if w.LatestID > 0 {
309+ b.Put(relLatestKey(w.Repo), []byte(strconv.FormatInt(w.LatestID, 10)))
310+ } else {
311+ b.Delete(relLatestKey(w.Repo))
312+ }
313+ }
288314 snap := c.db.Snapshot(ctx)
289315 for _, r := range w.DeleteReleases {
290316 if r.Repo != w.Repo || r.Version == 0 {
@@ -388,7 +414,7 @@ func (c *Releases) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) erro
388414
389415 // releaseKeys collects all repository-owned release keys, including global indexes.
390416 func releaseKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName) [][]byte {
391- keys := [][]byte{relUseKey(repo)}
417+ keys := [][]byte{relUseKey(repo), relLatestKey(repo)}
392418 for _, prefix := range []string{"rel/", "reltag/", "relassets/"} {
393419 for key, item := range snap.Prefix(ctx, []byte(prefix+repo.String()+"/")) {
394420 keys = append(keys, slices.Clone(key))
@@ -36,7 +36,26 @@ func relNamePrefix(repo domain.RepoName, id int64) string {
36 func relNameKey(a domain.ReleaseAsset) []byte {36 func relNameKey(a domain.ReleaseAsset) []byte {
37 return []byte(relNamePrefix(a.Repo, a.ReleaseID) + a.Name)37 return []byte(relNamePrefix(a.Repo, a.ReleaseID) + a.Name)
38 }38 }
39-func relUseKey(repo domain.RepoName) []byte { return []byte("reluse/" + repo.String()) }39+func relLatestKey(repo domain.RepoName) []byte { return []byte("rellatest/" + repo.String()) }
40+func relUseKey(repo domain.RepoName) []byte { return []byte("reluse/" + repo.String()) }
41+
42+// GetLatestRelease resolves the index and release from the same snapshot.
43+func (c *Releases) GetLatestRelease(ctx context.Context, repo domain.RepoName) (domain.Release, error) {
44+ snap := c.db.Snapshot(ctx)
45+ item, ok := snap.Get(ctx, relLatestKey(repo))
46+ if !ok {
47+ return domain.Release{}, domain.ErrNotFound
48+ }
49+ id, err := strconv.ParseInt(string(item.Value), 10, 64)
50+ if err != nil {
51+ return domain.Release{}, err
52+ }
53+ item, ok = snap.Get(ctx, relKey(repo, id))
54+ if !ok {
55+ return domain.Release{}, domain.ErrNotFound
56+ }
57+ return decodeRelease(item)
58+}
40 func encodeRelease(r domain.Release) []byte {59 func encodeRelease(r domain.Release) []byte {
41 return mustMarshal(&storagepb.Release{60 return mustMarshal(&storagepb.Release{
42 Id: r.ID,61 Id: r.ID,
@@ -285,6 +304,13 @@ func (c *Releases) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) erro
285 b.RequireVersion(relUseKey(w.Repo), w.UsageVersion)304 b.RequireVersion(relUseKey(w.Repo), w.UsageVersion)
286 }305 }
287 b.Put(relUseKey(w.Repo), mustMarshal(&storagepb.ReleaseUsage{Used: w.Used}))306 b.Put(relUseKey(w.Repo), mustMarshal(&storagepb.ReleaseUsage{Used: w.Used}))
307+ if w.SetLatest {
308+ if w.LatestID > 0 {
309+ b.Put(relLatestKey(w.Repo), []byte(strconv.FormatInt(w.LatestID, 10)))
310+ } else {
311+ b.Delete(relLatestKey(w.Repo))
312+ }
313+ }
288 snap := c.db.Snapshot(ctx)314 snap := c.db.Snapshot(ctx)
289 for _, r := range w.DeleteReleases {315 for _, r := range w.DeleteReleases {
290 if r.Repo != w.Repo || r.Version == 0 {316 if r.Repo != w.Repo || r.Version == 0 {
@@ -388,7 +414,7 @@ func (c *Releases) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) erro
388 414
389 // releaseKeys collects all repository-owned release keys, including global indexes.415 // releaseKeys collects all repository-owned release keys, including global indexes.
390 func releaseKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName) [][]byte {416 func releaseKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName) [][]byte {
391- keys := [][]byte{relUseKey(repo)}417+ keys := [][]byte{relUseKey(repo), relLatestKey(repo)}
392 for _, prefix := range []string{"rel/", "reltag/", "relassets/"} {418 for _, prefix := range []string{"rel/", "reltag/", "relassets/"} {
393 for key, item := range snap.Prefix(ctx, []byte(prefix+repo.String()+"/")) {419 for key, item := range snap.Prefix(ctx, []byte(prefix+repo.String()+"/")) {
394 keys = append(keys, slices.Clone(key))420 keys = append(keys, slices.Clone(key))
A internal/adapters/kvstore/releases_index.go +67 -0
@@ -0,0 +1,67 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package kvstore
3+
4+import (
5+ "context"
6+ "fmt"
7+ "strconv"
8+
9+ kv "bnema.dev/ermite-kv"
10+ "bnema.dev/ermite/internal/domain"
11+)
12+
13+const releaseLatestIndexMarker = "meta/release-latest-index"
14+
15+// buildReleaseLatestIndex runs before the store is exposed to writers. A crash
16+// before the marker is written safely repeats the pass on the next open.
17+// Selection remains owned by domain.LatestRelease.
18+func (s *Store) buildReleaseLatestIndex(ctx context.Context) error {
19+ if _, ok := s.db.Get(ctx, []byte(releaseLatestIndexMarker)); ok {
20+ return nil
21+ }
22+ var repo domain.RepoName
23+ var best domain.Release
24+ var eligible bool
25+ flush := func() error {
26+ var b kv.Batch
27+ if eligible {
28+ b.Put(relLatestKey(repo), []byte(strconv.FormatInt(best.ID, 10)))
29+ } else {
30+ b.Delete(relLatestKey(repo))
31+ }
32+ if _, err := s.db.Apply(ctx, &b); err != nil {
33+ return fmt.Errorf("build latest release index: %w", err)
34+ }
35+ return nil
36+ }
37+ haveRepo := false
38+ for _, item := range s.db.Snapshot(ctx).Prefix(ctx, []byte("rel/")) {
39+ rel, err := decodeRelease(item)
40+ if err != nil {
41+ return fmt.Errorf("build latest release index: %w", err)
42+ }
43+ if haveRepo && rel.Repo != repo {
44+ if err := flush(); err != nil {
45+ return err
46+ }
47+ best, eligible = domain.Release{}, false
48+ }
49+ repo, haveRepo = rel.Repo, true
50+ if eligible {
51+ best, eligible = domain.LatestRelease([]domain.Release{best, rel})
52+ } else {
53+ best, eligible = domain.LatestRelease([]domain.Release{rel})
54+ }
55+ }
56+ if haveRepo {
57+ if err := flush(); err != nil {
58+ return err
59+ }
60+ }
61+ var b kv.Batch
62+ b.Put([]byte(releaseLatestIndexMarker), []byte{1})
63+ if _, err := s.db.Apply(ctx, &b); err != nil {
64+ return fmt.Errorf("mark latest release index: %w", err)
65+ }
66+ return nil
67+}
@@ -0,0 +1,67 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package kvstore
3+
4+import (
5+ "context"
6+ "fmt"
7+ "strconv"
8+
9+ kv "bnema.dev/ermite-kv"
10+ "bnema.dev/ermite/internal/domain"
11+)
12+
13+const releaseLatestIndexMarker = "meta/release-latest-index"
14+
15+// buildReleaseLatestIndex runs before the store is exposed to writers. A crash
16+// before the marker is written safely repeats the pass on the next open.
17+// Selection remains owned by domain.LatestRelease.
18+func (s *Store) buildReleaseLatestIndex(ctx context.Context) error {
19+ if _, ok := s.db.Get(ctx, []byte(releaseLatestIndexMarker)); ok {
20+ return nil
21+ }
22+ var repo domain.RepoName
23+ var best domain.Release
24+ var eligible bool
25+ flush := func() error {
26+ var b kv.Batch
27+ if eligible {
28+ b.Put(relLatestKey(repo), []byte(strconv.FormatInt(best.ID, 10)))
29+ } else {
30+ b.Delete(relLatestKey(repo))
31+ }
32+ if _, err := s.db.Apply(ctx, &b); err != nil {
33+ return fmt.Errorf("build latest release index: %w", err)
34+ }
35+ return nil
36+ }
37+ haveRepo := false
38+ for _, item := range s.db.Snapshot(ctx).Prefix(ctx, []byte("rel/")) {
39+ rel, err := decodeRelease(item)
40+ if err != nil {
41+ return fmt.Errorf("build latest release index: %w", err)
42+ }
43+ if haveRepo && rel.Repo != repo {
44+ if err := flush(); err != nil {
45+ return err
46+ }
47+ best, eligible = domain.Release{}, false
48+ }
49+ repo, haveRepo = rel.Repo, true
50+ if eligible {
51+ best, eligible = domain.LatestRelease([]domain.Release{best, rel})
52+ } else {
53+ best, eligible = domain.LatestRelease([]domain.Release{rel})
54+ }
55+ }
56+ if haveRepo {
57+ if err := flush(); err != nil {
58+ return err
59+ }
60+ }
61+ var b kv.Batch
62+ b.Put([]byte(releaseLatestIndexMarker), []byte{1})
63+ if _, err := s.db.Apply(ctx, &b); err != nil {
64+ return fmt.Errorf("mark latest release index: %w", err)
65+ }
66+ return nil
67+}
A internal/adapters/kvstore/releases_index_test.go +97 -0
@@ -0,0 +1,97 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package kvstore
3+
4+import (
5+ "context"
6+ "testing"
7+ "time"
8+
9+ kv "bnema.dev/ermite-kv"
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+func TestReleaseLatestIndexBackfill(t *testing.T) {
16+ ctx := t.Context()
17+ dir := t.TempDir()
18+ s, err := Open(ctx, dir)
19+ require.NoError(t, err)
20+ var repos []domain.RepoName
21+ for _, raw := range []string{"backfill", "empty", "newest"} {
22+ repo, err := domain.ParseRepoName(raw)
23+ require.NoError(t, err)
24+ repos = append(repos, repo)
25+ }
26+ for _, repo := range repos {
27+ require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}))
28+ }
29+ var b kv.Batch
30+ for i, repo := range repos {
31+ for j := 1; j <= 5; j++ {
32+ rel := domain.Release{ID: int64(i*10 + j), Repo: repo, Tag: string(rune('a' + j)), CreatedAt: time.Unix(int64(j), 0)}
33+ rel.Draft = j == 5 || i == 1
34+ rel.Prerelease = j == 4
35+ rel.MakeLatest = j == 1 && i == 0
36+ b.Put(relKey(repo, rel.ID), encodeRelease(rel))
37+ }
38+ // A stale key must also be removed for a repo with no eligible release.
39+ b.Put(relLatestKey(repo), []byte("999"))
40+ }
41+ b.Delete([]byte(releaseLatestIndexMarker))
42+ _, err = s.db.Apply(ctx, &b)
43+ require.NoError(t, err)
44+ require.NoError(t, s.Close(context.Background()))
45+ s, err = Open(ctx, dir)
46+ require.NoError(t, err)
47+ defer func() { require.NoError(t, s.Close(context.Background())) }()
48+ for i, want := range []int64{1, 0, 23} {
49+ got, err := s.Releases().GetLatestRelease(ctx, repos[i])
50+ if want == 0 {
51+ require.ErrorIs(t, err, domain.ErrNotFound)
52+ } else {
53+ require.NoError(t, err)
54+ require.Equal(t, want, got.ID)
55+ }
56+ }
57+ require.NoError(t, s.buildReleaseLatestIndex(ctx))
58+ got, err := s.Releases().GetLatestRelease(ctx, repos[0])
59+ require.NoError(t, err)
60+ require.EqualValues(t, 1, got.ID)
61+ require.NoError(t, s.Delete(ctx, repos[0]))
62+ _, err = s.Releases().GetLatestRelease(ctx, repos[0])
63+ require.ErrorIs(t, err, domain.ErrNotFound)
64+}
65+
66+func TestGetLatestReleaseAtomicUpdate(t *testing.T) {
67+ ctx := t.Context()
68+ s, err := Open(ctx, t.TempDir())
69+ require.NoError(t, err)
70+ defer func() { require.NoError(t, s.Close(context.Background())) }()
71+ repo, err := domain.ParseRepoName("atomic")
72+ require.NoError(t, err)
73+ require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}))
74+ a := domain.Release{ID: 1, Repo: repo, Tag: "a"}
75+ b := domain.Release{ID: 2, Repo: repo, Tag: "b"}
76+ store := s.Releases()
77+ require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, PutReleases: []domain.Release{a, b}, SetLatest: true, LatestID: a.ID}))
78+ a, err = store.GetLatestRelease(ctx, repo)
79+ require.NoError(t, err)
80+ require.EqualValues(t, 1, a.ID)
81+ _, version, err := store.ReleaseUsage(ctx, repo)
82+ require.NoError(t, err)
83+ a.Prerelease = true
84+ require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutReleases: []domain.Release{a}, SetLatest: true, LatestID: b.ID}))
85+ got, err := store.GetLatestRelease(ctx, repo)
86+ require.NoError(t, err)
87+ require.EqualValues(t, 2, got.ID)
88+ // An asset-only/usage write leaves the index untouched, including its version.
89+ before, ok := s.db.Get(ctx, relLatestKey(repo))
90+ require.True(t, ok)
91+ _, version, err = store.ReleaseUsage(ctx, repo)
92+ require.NoError(t, err)
93+ require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, Used: 10}))
94+ after, ok := s.db.Get(ctx, relLatestKey(repo))
95+ require.True(t, ok)
96+ require.Equal(t, before, after)
97+}
@@ -0,0 +1,97 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package kvstore
3+
4+import (
5+ "context"
6+ "testing"
7+ "time"
8+
9+ kv "bnema.dev/ermite-kv"
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+func TestReleaseLatestIndexBackfill(t *testing.T) {
16+ ctx := t.Context()
17+ dir := t.TempDir()
18+ s, err := Open(ctx, dir)
19+ require.NoError(t, err)
20+ var repos []domain.RepoName
21+ for _, raw := range []string{"backfill", "empty", "newest"} {
22+ repo, err := domain.ParseRepoName(raw)
23+ require.NoError(t, err)
24+ repos = append(repos, repo)
25+ }
26+ for _, repo := range repos {
27+ require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}))
28+ }
29+ var b kv.Batch
30+ for i, repo := range repos {
31+ for j := 1; j <= 5; j++ {
32+ rel := domain.Release{ID: int64(i*10 + j), Repo: repo, Tag: string(rune('a' + j)), CreatedAt: time.Unix(int64(j), 0)}
33+ rel.Draft = j == 5 || i == 1
34+ rel.Prerelease = j == 4
35+ rel.MakeLatest = j == 1 && i == 0
36+ b.Put(relKey(repo, rel.ID), encodeRelease(rel))
37+ }
38+ // A stale key must also be removed for a repo with no eligible release.
39+ b.Put(relLatestKey(repo), []byte("999"))
40+ }
41+ b.Delete([]byte(releaseLatestIndexMarker))
42+ _, err = s.db.Apply(ctx, &b)
43+ require.NoError(t, err)
44+ require.NoError(t, s.Close(context.Background()))
45+ s, err = Open(ctx, dir)
46+ require.NoError(t, err)
47+ defer func() { require.NoError(t, s.Close(context.Background())) }()
48+ for i, want := range []int64{1, 0, 23} {
49+ got, err := s.Releases().GetLatestRelease(ctx, repos[i])
50+ if want == 0 {
51+ require.ErrorIs(t, err, domain.ErrNotFound)
52+ } else {
53+ require.NoError(t, err)
54+ require.Equal(t, want, got.ID)
55+ }
56+ }
57+ require.NoError(t, s.buildReleaseLatestIndex(ctx))
58+ got, err := s.Releases().GetLatestRelease(ctx, repos[0])
59+ require.NoError(t, err)
60+ require.EqualValues(t, 1, got.ID)
61+ require.NoError(t, s.Delete(ctx, repos[0]))
62+ _, err = s.Releases().GetLatestRelease(ctx, repos[0])
63+ require.ErrorIs(t, err, domain.ErrNotFound)
64+}
65+
66+func TestGetLatestReleaseAtomicUpdate(t *testing.T) {
67+ ctx := t.Context()
68+ s, err := Open(ctx, t.TempDir())
69+ require.NoError(t, err)
70+ defer func() { require.NoError(t, s.Close(context.Background())) }()
71+ repo, err := domain.ParseRepoName("atomic")
72+ require.NoError(t, err)
73+ require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}))
74+ a := domain.Release{ID: 1, Repo: repo, Tag: "a"}
75+ b := domain.Release{ID: 2, Repo: repo, Tag: "b"}
76+ store := s.Releases()
77+ require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, PutReleases: []domain.Release{a, b}, SetLatest: true, LatestID: a.ID}))
78+ a, err = store.GetLatestRelease(ctx, repo)
79+ require.NoError(t, err)
80+ require.EqualValues(t, 1, a.ID)
81+ _, version, err := store.ReleaseUsage(ctx, repo)
82+ require.NoError(t, err)
83+ a.Prerelease = true
84+ require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutReleases: []domain.Release{a}, SetLatest: true, LatestID: b.ID}))
85+ got, err := store.GetLatestRelease(ctx, repo)
86+ require.NoError(t, err)
87+ require.EqualValues(t, 2, got.ID)
88+ // An asset-only/usage write leaves the index untouched, including its version.
89+ before, ok := s.db.Get(ctx, relLatestKey(repo))
90+ require.True(t, ok)
91+ _, version, err = store.ReleaseUsage(ctx, repo)
92+ require.NoError(t, err)
93+ require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, Used: 10}))
94+ after, ok := s.db.Get(ctx, relLatestKey(repo))
95+ require.True(t, ok)
96+ require.Equal(t, before, after)
97+}
M internal/adapters/kvstore/store.go +6 -1
@@ -43,7 +43,8 @@ import (
4343 // holding all eleven previous scopes, preserving their owner rights. Version 6
4444 // adds releases and release:write; older builds cannot clean up their metadata
4545 // or preserve the owner rights of upgraded tokens (see upgradeReleaseTokens).
46-const SchemaVersion = 6
46+// Version 7 adds the latest-release index; older writers would leave it stale.
47+const SchemaVersion = 7
4748
4849 const schemaKey = "meta/schema"
4950
@@ -76,6 +77,10 @@ func Open(ctx context.Context, dir string) (*Store, error) {
7677 _ = db.Close(context.WithoutCancel(ctx))
7778 return nil, err
7879 }
80+ if err := s.buildReleaseLatestIndex(ctx); err != nil {
81+ _ = db.Close(context.WithoutCancel(ctx))
82+ return nil, err
83+ }
7984 return s, nil
8085 }
8186
@@ -43,7 +43,8 @@ import (
43 // holding all eleven previous scopes, preserving their owner rights. Version 643 // holding all eleven previous scopes, preserving their owner rights. Version 6
44 // adds releases and release:write; older builds cannot clean up their metadata44 // adds releases and release:write; older builds cannot clean up their metadata
45 // or preserve the owner rights of upgraded tokens (see upgradeReleaseTokens).45 // or preserve the owner rights of upgraded tokens (see upgradeReleaseTokens).
46-const SchemaVersion = 646+// Version 7 adds the latest-release index; older writers would leave it stale.
47+const SchemaVersion = 7
47 48
48 const schemaKey = "meta/schema"49 const schemaKey = "meta/schema"
49 50
@@ -76,6 +77,10 @@ func Open(ctx context.Context, dir string) (*Store, error) {
76 _ = db.Close(context.WithoutCancel(ctx))77 _ = db.Close(context.WithoutCancel(ctx))
77 return nil, err78 return nil, err
78 }79 }
80+ if err := s.buildReleaseLatestIndex(ctx); err != nil {
81+ _ = db.Close(context.WithoutCancel(ctx))
82+ return nil, err
83+ }
79 return s, nil84 return s, nil
80 }85 }
81 86
M internal/adapters/web/fragments.go +1 -1
@@ -167,7 +167,7 @@ func (l *liveStream) gone(ctx context.Context, name string) ([]fragment, error)
167167 func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {
168168 switch ev := we.Event.(type) {
169169 case domain.ReleasePublished, domain.ReleaseUpdated, domain.ReleaseDeleted, domain.ReleaseUnpublished:
170- if l.view == "releases" || l.view == "release" {
170+ if l.view == "releases" || l.view == "release" || l.view == "summary" {
171171 return gap, nil // notes, assets, latest or visibility changed: reload
172172 }
173173 return nil, nil
@@ -167,7 +167,7 @@ func (l *liveStream) gone(ctx context.Context, name string) ([]fragment, error)
167 func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {167 func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {
168 switch ev := we.Event.(type) {168 switch ev := we.Event.(type) {
169 case domain.ReleasePublished, domain.ReleaseUpdated, domain.ReleaseDeleted, domain.ReleaseUnpublished:169 case domain.ReleasePublished, domain.ReleaseUpdated, domain.ReleaseDeleted, domain.ReleaseUnpublished:
170- if l.view == "releases" || l.view == "release" {170+ if l.view == "releases" || l.view == "release" || l.view == "summary" {
171 return gap, nil // notes, assets, latest or visibility changed: reload171 return gap, nil // notes, assets, latest or visibility changed: reload
172 }172 }
173 return nil, nil173 return nil, nil
M internal/adapters/web/fragments_test.go +3 -3
@@ -101,8 +101,8 @@ func TestMRPushedHeadSignal(t *testing.T) {
101101 }
102102 }
103103
104-// Release lifecycle events invalidate the release pages without putting notes
105-// or asset metadata in SSE payloads. Other views need no release fragments.
104+// Release lifecycle events invalidate release pages and the summary's release
105+// fact without putting notes or asset metadata in SSE payloads.
106106 func TestReleaseLiveUpdates(t *testing.T) {
107107 for _, ev := range []domain.Event{domain.ReleasePublished{}, domain.ReleaseUpdated{}, domain.ReleaseDeleted{}, domain.ReleaseUnpublished{}} {
108108 we := ports.WatchEvent{Event: ev}
@@ -111,7 +111,7 @@ func TestReleaseLiveUpdates(t *testing.T) {
111111 st := &liveStream{repo: "proj", view: view}
112112 got, err := st.render(t.Context(), we)
113113 require.NoError(t, err)
114- if view == "release" || view == "releases" {
114+ if view == "release" || view == "releases" || view == "summary" {
115115 assert.Equal(t, gap, got)
116116 } else {
117117 assert.Empty(t, got)
@@ -101,8 +101,8 @@ func TestMRPushedHeadSignal(t *testing.T) {
101 }101 }
102 }102 }
103 103
104-// Release lifecycle events invalidate the release pages without putting notes104+// Release lifecycle events invalidate release pages and the summary's release
105-// or asset metadata in SSE payloads. Other views need no release fragments.105+// fact without putting notes or asset metadata in SSE payloads.
106 func TestReleaseLiveUpdates(t *testing.T) {106 func TestReleaseLiveUpdates(t *testing.T) {
107 for _, ev := range []domain.Event{domain.ReleasePublished{}, domain.ReleaseUpdated{}, domain.ReleaseDeleted{}, domain.ReleaseUnpublished{}} {107 for _, ev := range []domain.Event{domain.ReleasePublished{}, domain.ReleaseUpdated{}, domain.ReleaseDeleted{}, domain.ReleaseUnpublished{}} {
108 we := ports.WatchEvent{Event: ev}108 we := ports.WatchEvent{Event: ev}
@@ -111,7 +111,7 @@ func TestReleaseLiveUpdates(t *testing.T) {
111 st := &liveStream{repo: "proj", view: view}111 st := &liveStream{repo: "proj", view: view}
112 got, err := st.render(t.Context(), we)112 got, err := st.render(t.Context(), we)
113 require.NoError(t, err)113 require.NoError(t, err)
114- if view == "release" || view == "releases" {114+ if view == "release" || view == "releases" || view == "summary" {
115 assert.Equal(t, gap, got)115 assert.Equal(t, gap, got)
116 } else {116 } else {
117 assert.Empty(t, got)117 assert.Empty(t, got)
M internal/adapters/web/issues_test.go +3 -3
@@ -100,7 +100,7 @@ func TestIssueList(t *testing.T) {
100100 assert.Contains(t, body, "crash &lt;b&gt;on&lt;/b&gt; start")
101101 assert.Contains(t, body, `href="/proj/issues?page=tok2"`, "older page")
102102 assert.Contains(t, body, `href="/proj/issues?state=closed"`, "closed tab")
103- assert.Contains(t, body, `href="/proj/issues" data-key="i"`, "the Issues tab")
103+ assert.Contains(t, body, `href="/proj/issues" data-key="g i"`, "the Issues tab")
104104 assert.NotContains(t, body, "New issue", "no form for an anonymous visitor")
105105 assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"))
106106
@@ -169,7 +169,7 @@ func TestIssueNotFound(t *testing.T) {
169169 }
170170 summary := off.do("GET", "/proj", false, nil)
171171 require.Equal(t, 200, summary.Code)
172- assert.NotContains(t, summary.Body.String(), `data-key="i"`)
172+ assert.NotContains(t, summary.Body.String(), `data-key="g i"`)
173173
174174 // The mode is unset in the settings: off as well.
175175 unset := newIssueRig(t, "", false)
@@ -257,7 +257,7 @@ func TestIssueRoutesAbsentWithoutService(t *testing.T) {
257257 }
258258 rec := httptest.NewRecorder()
259259 srv.ServeHTTP(rec, httptest.NewRequest("GET", "/proj", nil))
260- assert.NotContains(t, rec.Body.String(), `data-key="i"`)
260+ assert.NotContains(t, rec.Body.String(), `data-key="g i"`)
261261 }
262262
263263 func TestIssueNumberRedirects(t *testing.T) {
@@ -100,7 +100,7 @@ func TestIssueList(t *testing.T) {
100 assert.Contains(t, body, "crash &lt;b&gt;on&lt;/b&gt; start")100 assert.Contains(t, body, "crash &lt;b&gt;on&lt;/b&gt; start")
101 assert.Contains(t, body, `href="/proj/issues?page=tok2"`, "older page")101 assert.Contains(t, body, `href="/proj/issues?page=tok2"`, "older page")
102 assert.Contains(t, body, `href="/proj/issues?state=closed"`, "closed tab")102 assert.Contains(t, body, `href="/proj/issues?state=closed"`, "closed tab")
103- assert.Contains(t, body, `href="/proj/issues" data-key="i"`, "the Issues tab")103+ assert.Contains(t, body, `href="/proj/issues" data-key="g i"`, "the Issues tab")
104 assert.NotContains(t, body, "New issue", "no form for an anonymous visitor")104 assert.NotContains(t, body, "New issue", "no form for an anonymous visitor")
105 assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"))105 assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"))
106 106
@@ -169,7 +169,7 @@ func TestIssueNotFound(t *testing.T) {
169 }169 }
170 summary := off.do("GET", "/proj", false, nil)170 summary := off.do("GET", "/proj", false, nil)
171 require.Equal(t, 200, summary.Code)171 require.Equal(t, 200, summary.Code)
172- assert.NotContains(t, summary.Body.String(), `data-key="i"`)172+ assert.NotContains(t, summary.Body.String(), `data-key="g i"`)
173 173
174 // The mode is unset in the settings: off as well.174 // The mode is unset in the settings: off as well.
175 unset := newIssueRig(t, "", false)175 unset := newIssueRig(t, "", false)
@@ -257,7 +257,7 @@ func TestIssueRoutesAbsentWithoutService(t *testing.T) {
257 }257 }
258 rec := httptest.NewRecorder()258 rec := httptest.NewRecorder()
259 srv.ServeHTTP(rec, httptest.NewRequest("GET", "/proj", nil))259 srv.ServeHTTP(rec, httptest.NewRequest("GET", "/proj", nil))
260- assert.NotContains(t, rec.Body.String(), `data-key="i"`)260+ assert.NotContains(t, rec.Body.String(), `data-key="g i"`)
261 }261 }
262 262
263 func TestIssueNumberRedirects(t *testing.T) {263 func TestIssueNumberRedirects(t *testing.T) {
A internal/adapters/web/jstest/keyboard.test.mjs +76 -0
@@ -0,0 +1,76 @@
1+// Exercise the production key handler without a browser or dependencies.
2+import assert from 'node:assert/strict';
3+import { readFileSync } from 'node:fs';
4+import { test } from 'node:test';
5+import { runInNewContext } from 'node:vm';
6+const source = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
7+function keyboard({ owner = true, off = false, typing = false, dialog = false } = {}) {
8+ let handler;
9+ const fired = [];
10+ const timers = new Map();
11+ let timerID = 0;
12+ runInNewContext(source.slice(source.indexOf(' const single ='), source.indexOf(' // 2. A [data-fire]')), {
13+ addEventListener: (_, fn) => { handler = fn; },
14+ window: { ermiteKeys: { blocks: () => off } },
15+ ermite: { fire: name => fired.push(name) },
16+ $: selector => (owner && selector === '[data-inbox]') || (dialog && selector === 'dialog[open]'),
17+ closeMenus: () => false,
18+ clearTimeout: id => timers.delete(id),
19+ setTimeout: fn => { timers.set(++timerID, fn); return timerID; },
20+ });
21+ return { fired, expireTimers() {
22+ const callbacks = [...timers.values()];
23+ timers.clear();
24+ for (const callback of callbacks) callback();
25+ }, press(key, shiftKey = false) {
26+ handler({ key, shiftKey, target: { closest: selector => typing && selector.startsWith('input'), blur() {} }, preventDefault() {} });
27+ } };
28+}
29+test('sections require g and have no old t alias', () => {
30+ const k = keyboard();
31+ for (const key of ['s', 't', 'c', 'r', 'm', 'i']) k.press(key);
32+ assert.deepEqual(k.fired, []);
33+ for (const key of ['s', 'f', 'c', 'b', 'm', 'i', 'r', 'a', 'h', 'g', 't']) { k.press('g'); k.press(key); }
34+ assert.deepEqual(k.fired, ['s', 'f', 'c', 'b', 'm', 'i', 'r', 'a', 'gh', 'top']);
35+});
36+test('uppercase actions work with either reported casing and interrupt prefixes', () => {
37+ for (const [key, action] of Object.entries({ G: 'bottom', M: 'merge', R: 'read', E: 'expandAll', I: 'inbox' })) {
38+ for (const reported of [key, key.toLowerCase()]) {
39+ const k = keyboard();
40+ k.press('g'); k.press(reported, true); k.press(reported, true);
41+ assert.deepEqual(k.fired, [action, action]);
42+ }
43+ }
44+});
45+test('uppercase actions respect preference, fields and dialogs', () => {
46+ for (const options of [{ off: true }, { typing: true }, { dialog: true }]) {
47+ const k = keyboard(options); k.press('g', true);
48+ assert.deepEqual(k.fired, []);
49+ }
50+});
51+
52+test('Shift+I opens inbox while issues need the g i sequence', () => {
53+ const k = keyboard();
54+ k.press('I', true); k.press('i');
55+ assert.deepEqual(k.fired, ['inbox']);
56+ k.press('g'); k.press('i');
57+ assert.deepEqual(k.fired, ['inbox', 'i']);
58+});
59+test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
60+ for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
61+ const k = keyboard(options); k.press('I', true);
62+ assert.deepEqual(k.fired, [], JSON.stringify(options));
63+ }
64+});
65+test('shifted punctuation still opens help and the command palette', () => {
66+ const k = keyboard();
67+ k.press('?', true); k.press(':', true);
68+ assert.deepEqual(k.fired, ['help', 'cmd']);
69+});
70+test('g prefix expires without scheduling real timers', () => {
71+ const k = keyboard();
72+ k.press('g'); k.expireTimers(); k.press('s');
73+ assert.deepEqual(k.fired, []);
74+ k.press('g'); k.press('s');
75+ assert.deepEqual(k.fired, ['s']);
76+});
@@ -0,0 +1,76 @@
1+// Exercise the production key handler without a browser or dependencies.
2+import assert from 'node:assert/strict';
3+import { readFileSync } from 'node:fs';
4+import { test } from 'node:test';
5+import { runInNewContext } from 'node:vm';
6+const source = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
7+function keyboard({ owner = true, off = false, typing = false, dialog = false } = {}) {
8+ let handler;
9+ const fired = [];
10+ const timers = new Map();
11+ let timerID = 0;
12+ runInNewContext(source.slice(source.indexOf(' const single ='), source.indexOf(' // 2. A [data-fire]')), {
13+ addEventListener: (_, fn) => { handler = fn; },
14+ window: { ermiteKeys: { blocks: () => off } },
15+ ermite: { fire: name => fired.push(name) },
16+ $: selector => (owner && selector === '[data-inbox]') || (dialog && selector === 'dialog[open]'),
17+ closeMenus: () => false,
18+ clearTimeout: id => timers.delete(id),
19+ setTimeout: fn => { timers.set(++timerID, fn); return timerID; },
20+ });
21+ return { fired, expireTimers() {
22+ const callbacks = [...timers.values()];
23+ timers.clear();
24+ for (const callback of callbacks) callback();
25+ }, press(key, shiftKey = false) {
26+ handler({ key, shiftKey, target: { closest: selector => typing && selector.startsWith('input'), blur() {} }, preventDefault() {} });
27+ } };
28+}
29+test('sections require g and have no old t alias', () => {
30+ const k = keyboard();
31+ for (const key of ['s', 't', 'c', 'r', 'm', 'i']) k.press(key);
32+ assert.deepEqual(k.fired, []);
33+ for (const key of ['s', 'f', 'c', 'b', 'm', 'i', 'r', 'a', 'h', 'g', 't']) { k.press('g'); k.press(key); }
34+ assert.deepEqual(k.fired, ['s', 'f', 'c', 'b', 'm', 'i', 'r', 'a', 'gh', 'top']);
35+});
36+test('uppercase actions work with either reported casing and interrupt prefixes', () => {
37+ for (const [key, action] of Object.entries({ G: 'bottom', M: 'merge', R: 'read', E: 'expandAll', I: 'inbox' })) {
38+ for (const reported of [key, key.toLowerCase()]) {
39+ const k = keyboard();
40+ k.press('g'); k.press(reported, true); k.press(reported, true);
41+ assert.deepEqual(k.fired, [action, action]);
42+ }
43+ }
44+});
45+test('uppercase actions respect preference, fields and dialogs', () => {
46+ for (const options of [{ off: true }, { typing: true }, { dialog: true }]) {
47+ const k = keyboard(options); k.press('g', true);
48+ assert.deepEqual(k.fired, []);
49+ }
50+});
51+
52+test('Shift+I opens inbox while issues need the g i sequence', () => {
53+ const k = keyboard();
54+ k.press('I', true); k.press('i');
55+ assert.deepEqual(k.fired, ['inbox']);
56+ k.press('g'); k.press('i');
57+ assert.deepEqual(k.fired, ['inbox', 'i']);
58+});
59+test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
60+ for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
61+ const k = keyboard(options); k.press('I', true);
62+ assert.deepEqual(k.fired, [], JSON.stringify(options));
63+ }
64+});
65+test('shifted punctuation still opens help and the command palette', () => {
66+ const k = keyboard();
67+ k.press('?', true); k.press(':', true);
68+ assert.deepEqual(k.fired, ['help', 'cmd']);
69+});
70+test('g prefix expires without scheduling real timers', () => {
71+ const k = keyboard();
72+ k.press('g'); k.expireTimers(); k.press('s');
73+ assert.deepEqual(k.fired, []);
74+ k.press('g'); k.press('s');
75+ assert.deepEqual(k.fired, ['s']);
76+});
D internal/adapters/web/jstest/keys.test.mjs +0 -29
@@ -1,29 +0,0 @@
1-// Exercise the production keydown handler without loading the rest of the page.
2-import assert from 'node:assert/strict';
3-import { readFileSync } from 'node:fs';
4-import { test } from 'node:test';
5-const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
6-const block = src.slice(src.indexOf(' // 1. Keys.'), src.indexOf(' // 2. A [data-fire]'));
7-
8-function page({ owner = true, off = false, typing = false, dialog = false } = {}) {
9- let handler;
10- const fired = [];
11- const window = { ermiteKeys: { blocks: () => off } };
12- const $ = (s) => s === 'dialog[open]' ? dialog : s === '[data-inbox]' ? owner : null;
13- new Function('window', '$', 'ermite', 'addEventListener', block)(window, $, { fire: (n) => fired.push(n) }, (_, f) => { handler = f; });
14- return (key) => {
15- const e = { key, shiftKey: key === 'I', preventDefault() {}, target: { closest: (s) => s.startsWith('input') && typing } };
16- handler(e);
17- return fired;
18- };
19-}
20-test('Shift+I opens inbox while lowercase i retains the issues tab', () => {
21- const press = page();
22- assert.deepEqual(press('I'), ['inbox']);
23- assert.deepEqual(press('i'), ['inbox', 'i']);
24-});
25-test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
26- for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
27- assert.deepEqual(page(options)('I'), [], JSON.stringify(options));
28- }
29-});
@@ -1,29 +0,0 @@
1-// Exercise the production keydown handler without loading the rest of the page.
2-import assert from 'node:assert/strict';
3-import { readFileSync } from 'node:fs';
4-import { test } from 'node:test';
5-const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
6-const block = src.slice(src.indexOf(' // 1. Keys.'), src.indexOf(' // 2. A [data-fire]'));
7-
8-function page({ owner = true, off = false, typing = false, dialog = false } = {}) {
9- let handler;
10- const fired = [];
11- const window = { ermiteKeys: { blocks: () => off } };
12- const $ = (s) => s === 'dialog[open]' ? dialog : s === '[data-inbox]' ? owner : null;
13- new Function('window', '$', 'ermite', 'addEventListener', block)(window, $, { fire: (n) => fired.push(n) }, (_, f) => { handler = f; });
14- return (key) => {
15- const e = { key, shiftKey: key === 'I', preventDefault() {}, target: { closest: (s) => s.startsWith('input') && typing } };
16- handler(e);
17- return fired;
18- };
19-}
20-test('Shift+I opens inbox while lowercase i retains the issues tab', () => {
21- const press = page();
22- assert.deepEqual(press('I'), ['inbox']);
23- assert.deepEqual(press('i'), ['inbox', 'i']);
24-});
25-test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
26- for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
27- assert.deepEqual(page(options)('I'), [], JSON.stringify(options));
28- }
29-});
M internal/adapters/web/keys.templ +3 -2
@@ -23,7 +23,9 @@ templ Keys() {
2323 k-filter from:body -> ermite.focusFilter();
2424 k-gh from:body -> ermite.go('.brand');
2525 k-s from:body -> ermite.tab('s');
26- k-t from:body -> ermite.tab('t');
26+ k-f from:body -> ermite.tab('f');
27+ k-b from:body -> ermite.tab('b');
28+ k-a from:body -> ermite.tab('a');
2729 k-c from:body -> ermite.tab('c');
2830 k-r from:body -> ermite.tab('r');
2931 k-m from:body -> ermite.tab('m');
@@ -39,7 +41,6 @@ templ Keys() {
3941 k-permalink from:body -> ermite.permalink();
4042 k-merge from:body -> window.ermiteMR?.openMerge();
4143 k-read from:body -> window.ermiteMR?.markRead();
42- k-gf from:body -> window.ermiteMR?.focusFiles();
4344 k-keys from:body -> ermite.toggleKeys()"
4445 ></div>
4546 }
@@ -23,7 +23,9 @@ templ Keys() {
23 k-filter from:body -> ermite.focusFilter();23 k-filter from:body -> ermite.focusFilter();
24 k-gh from:body -> ermite.go('.brand');24 k-gh from:body -> ermite.go('.brand');
25 k-s from:body -> ermite.tab('s');25 k-s from:body -> ermite.tab('s');
26- k-t from:body -> ermite.tab('t');26+ k-f from:body -> ermite.tab('f');
27+ k-b from:body -> ermite.tab('b');
28+ k-a from:body -> ermite.tab('a');
27 k-c from:body -> ermite.tab('c');29 k-c from:body -> ermite.tab('c');
28 k-r from:body -> ermite.tab('r');30 k-r from:body -> ermite.tab('r');
29 k-m from:body -> ermite.tab('m');31 k-m from:body -> ermite.tab('m');
@@ -39,7 +41,6 @@ templ Keys() {
39 k-permalink from:body -> ermite.permalink();41 k-permalink from:body -> ermite.permalink();
40 k-merge from:body -> window.ermiteMR?.openMerge();42 k-merge from:body -> window.ermiteMR?.openMerge();
41 k-read from:body -> window.ermiteMR?.markRead();43 k-read from:body -> window.ermiteMR?.markRead();
42- k-gf from:body -> window.ermiteMR?.focusFiles();
43 k-keys from:body -> ermite.toggleKeys()"44 k-keys from:body -> ermite.toggleKeys()"
44 ></div>45 ></div>
45 }46 }
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
A internal/adapters/web/keys_test.go +26 -0
@@ -0,0 +1,26 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+)
10+
11+func TestSectionShortcutSequences(t *testing.T) {
12+ sections := []Section{}
13+ for _, key := range []string{"s", "f", "c", "b", "m", "i", "r", "a"} {
14+ sections = append(sections, Section{Key: key, Label: key, Href: "/" + key})
15+ }
16+ header := renderToString(t, Header(Layout{Sections: sections}))
17+ bindings := renderToString(t, Keys())
18+ for _, section := range sections {
19+ assert.Contains(t, header, `data-key="g `+section.Key+`"`)
20+ assert.NotContains(t, header, `aria-keyshortcuts="g `+section.Key+`"`)
21+ assert.Contains(t, header, `<kbd aria-hidden="true">g `+section.Key+`</kbd>`)
22+ assert.Contains(t, bindings, "ermite.tab('"+section.Key+"')")
23+ }
24+ assert.NotContains(t, bindings, "k-gf")
25+ assert.NotContains(t, bindings, "k-t from:body")
26+}
@@ -0,0 +1,26 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+)
10+
11+func TestSectionShortcutSequences(t *testing.T) {
12+ sections := []Section{}
13+ for _, key := range []string{"s", "f", "c", "b", "m", "i", "r", "a"} {
14+ sections = append(sections, Section{Key: key, Label: key, Href: "/" + key})
15+ }
16+ header := renderToString(t, Header(Layout{Sections: sections}))
17+ bindings := renderToString(t, Keys())
18+ for _, section := range sections {
19+ assert.Contains(t, header, `data-key="g `+section.Key+`"`)
20+ assert.NotContains(t, header, `aria-keyshortcuts="g `+section.Key+`"`)
21+ assert.Contains(t, header, `<kbd aria-hidden="true">g `+section.Key+`</kbd>`)
22+ assert.Contains(t, bindings, "ermite.tab('"+section.Key+"')")
23+ }
24+ assert.NotContains(t, bindings, "k-gf")
25+ assert.NotContains(t, bindings, "k-t from:body")
26+}
M internal/adapters/web/mr.templ +1 -1
@@ -430,7 +430,7 @@ templ mrFooter(p MRPage) {
430430 templ mrRail(p MRPage) {
431431 <section aria-labelledby="r-files" class="r-files">
432432 <h2 id="r-files">
433- Files <span class="count">{ itoa(len(p.Files)) }</span><kbd aria-hidden="true">g f</kbd>
433+ Files <span class="count">{ itoa(len(p.Files)) }</span><kbd aria-hidden="true">l</kbd>
434434 <button type="button" class="btn icon fold" data-js aria-expanded="true" title="fold the list of files" hx-on:click="ermiteMR.foldFiles()">
435435 <span class="ico i-chevron" aria-hidden="true"></span><span class="sr">fold the list of files</span>
436436 </button>
@@ -430,7 +430,7 @@ templ mrFooter(p MRPage) {
430 templ mrRail(p MRPage) {430 templ mrRail(p MRPage) {
431 <section aria-labelledby="r-files" class="r-files">431 <section aria-labelledby="r-files" class="r-files">
432 <h2 id="r-files">432 <h2 id="r-files">
433- Files <span class="count">{ itoa(len(p.Files)) }</span><kbd aria-hidden="true">g f</kbd>433+ Files <span class="count">{ itoa(len(p.Files)) }</span><kbd aria-hidden="true">l</kbd>
434 <button type="button" class="btn icon fold" data-js aria-expanded="true" title="fold the list of files" hx-on:click="ermiteMR.foldFiles()">434 <button type="button" class="btn icon fold" data-js aria-expanded="true" title="fold the list of files" hx-on:click="ermiteMR.foldFiles()">
435 <span class="ico i-chevron" aria-hidden="true"></span><span class="sr">fold the list of files</span>435 <span class="ico i-chevron" aria-hidden="true"></span><span class="sr">fold the list of files</span>
436 </button>436 </button>
M internal/adapters/web/mr_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/mr_test.go +1 -1
@@ -157,7 +157,7 @@ func TestMRPanelsAndMenus(t *testing.T) {
157157 assert.Contains(t, footer, "more commits")
158158 assert.Contains(t, footer, "1/2 failed")
159159 rail := renderToString(t, mrRail(p))
160- assert.Contains(t, rail, "g f")
160+ assert.Contains(t, rail, `<kbd aria-hidden="true">l</kbd>`)
161161 for _, text := range []string{"Checks", "Commits", "Settings", "mr-checks"} {
162162 assert.NotContains(t, rail, text)
163163 }
@@ -157,7 +157,7 @@ func TestMRPanelsAndMenus(t *testing.T) {
157 assert.Contains(t, footer, "more commits")157 assert.Contains(t, footer, "more commits")
158 assert.Contains(t, footer, "1/2 failed")158 assert.Contains(t, footer, "1/2 failed")
159 rail := renderToString(t, mrRail(p))159 rail := renderToString(t, mrRail(p))
160- assert.Contains(t, rail, "g f")160+ assert.Contains(t, rail, `<kbd aria-hidden="true">l</kbd>`)
161 for _, text := range []string{"Checks", "Commits", "Settings", "mr-checks"} {161 for _, text := range []string{"Checks", "Commits", "Settings", "mr-checks"} {
162 assert.NotContains(t, rail, text)162 assert.NotContains(t, rail, text)
163 }163 }
M internal/adapters/web/overlays.templ +2 -2
@@ -26,7 +26,7 @@ templ OverlayHelp(mr bool) {
2626 <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div>
2727 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div>
2828 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>
29- <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd></div>
29+ <div><dt><kbd>g s</kbd> <kbd>g f</kbd> <kbd>g c</kbd> <kbd>g b</kbd> <kbd>g m</kbd> <kbd>g i</kbd> <kbd>g r</kbd> <kbd>g a</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd></div>
3030 if SignedIn(ctx) {
3131 <div><dt><kbd>I</kbd></dt><dd>inbox</dd></div>
3232 }
@@ -37,7 +37,7 @@ templ OverlayHelp(mr bool) {
3737 <div><dt><kbd>e</kbd> <kbd>E</kbd></dt><dd>diff: the whole file in view / every file, with the lines the hunks hide</dd></div>
3838 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>
3939 if mr {
40- <div><dt><kbd>g f</kbd></dt><dd>merge request: the list of files (j k move, enter opens, esc back)</dd></div>
40+ <div><dt><kbd>l</kbd></dt><dd>merge request: the list of files (j k move, enter opens, esc back)</dd></div>
4141 <div><dt><kbd>M</kbd></dt><dd>merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges</dd></div>
4242 <div><dt><kbd>R</kbd></dt><dd>merge request: mark the head read</dd></div>
4343 }
@@ -26,7 +26,7 @@ templ OverlayHelp(mr bool) {
26 <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div>26 <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div>
27 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div>27 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div>
28 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>28 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>
29- <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd></div>29+ <div><dt><kbd>g s</kbd> <kbd>g f</kbd> <kbd>g c</kbd> <kbd>g b</kbd> <kbd>g m</kbd> <kbd>g i</kbd> <kbd>g r</kbd> <kbd>g a</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd></div>
30 if SignedIn(ctx) {30 if SignedIn(ctx) {
31 <div><dt><kbd>I</kbd></dt><dd>inbox</dd></div>31 <div><dt><kbd>I</kbd></dt><dd>inbox</dd></div>
32 }32 }
@@ -37,7 +37,7 @@ templ OverlayHelp(mr bool) {
37 <div><dt><kbd>e</kbd> <kbd>E</kbd></dt><dd>diff: the whole file in view / every file, with the lines the hunks hide</dd></div>37 <div><dt><kbd>e</kbd> <kbd>E</kbd></dt><dd>diff: the whole file in view / every file, with the lines the hunks hide</dd></div>
38 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>38 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>
39 if mr {39 if mr {
40- <div><dt><kbd>g f</kbd></dt><dd>merge request: the list of files (j k move, enter opens, esc back)</dd></div>40+ <div><dt><kbd>l</kbd></dt><dd>merge request: the list of files (j k move, enter opens, esc back)</dd></div>
41 <div><dt><kbd>M</kbd></dt><dd>merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges</dd></div>41 <div><dt><kbd>M</kbd></dt><dd>merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges</dd></div>
42 <div><dt><kbd>R</kbd></dt><dd>merge request: mark the head read</dd></div>42 <div><dt><kbd>R</kbd></dt><dd>merge request: mark the head read</dd></div>
43 }43 }
M internal/adapters/web/overlays_templ.go +2 -2 generated file: collapsed
M internal/adapters/web/pages.templ +8 -0
@@ -115,6 +115,14 @@ templ summaryRail(p SummaryPage) {
115115 if p.Latest != nil {
116116 @latestTagFact(*p.Latest)
117117 }
118+ if r := p.Release; r != nil {
119+ <dt>release</dt>
120+ <dd>
121+ <a href={ templ.SafeURL(r.Href) }>{ r.Tag }</a>{ " · " }
122+ <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>{ " · " }
123+ <a href={ templ.SafeURL(r.AllHref) }>all releases</a>
124+ </dd>
125+ }
118126 </dl>
119127 </section>
120128 if len(p.Recent) > 0 {
@@ -115,6 +115,14 @@ templ summaryRail(p SummaryPage) {
115 if p.Latest != nil {115 if p.Latest != nil {
116 @latestTagFact(*p.Latest)116 @latestTagFact(*p.Latest)
117 }117 }
118+ if r := p.Release; r != nil {
119+ <dt>release</dt>
120+ <dd>
121+ <a href={ templ.SafeURL(r.Href) }>{ r.Tag }</a>{ " · " }
122+ <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>{ " · " }
123+ <a href={ templ.SafeURL(r.AllHref) }>all releases</a>
124+ </dd>
125+ }
118 </dl>126 </dl>
119 </section>127 </section>
120 if len(p.Recent) > 0 {128 if len(p.Recent) > 0 {
M internal/adapters/web/pages_templ.go +371 -261
@@ -534,25 +534,135 @@ func summaryRail(p SummaryPage) templ.Component {
534534 return templ_7745c5c3_Err
535535 }
536536 }
537- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</dl></section>")
537+ if r := p.Release; r != nil {
538+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<dt>release</dt><dd><a href=\"")
539+ if templ_7745c5c3_Err != nil {
540+ return templ_7745c5c3_Err
541+ }
542+ var templ_7745c5c3_Var27 templ.SafeURL
543+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
544+ if templ_7745c5c3_Err != nil {
545+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 36}
546+ }
547+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
548+ if templ_7745c5c3_Err != nil {
549+ return templ_7745c5c3_Err
550+ }
551+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\">")
552+ if templ_7745c5c3_Err != nil {
553+ return templ_7745c5c3_Err
554+ }
555+ var templ_7745c5c3_Var28 string
556+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(r.Tag)
557+ if templ_7745c5c3_Err != nil {
558+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 46}
559+ }
560+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
561+ if templ_7745c5c3_Err != nil {
562+ return templ_7745c5c3_Err
563+ }
564+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</a>")
565+ if templ_7745c5c3_Err != nil {
566+ return templ_7745c5c3_Err
567+ }
568+ var templ_7745c5c3_Var29 string
569+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
570+ if templ_7745c5c3_Err != nil {
571+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 60}
572+ }
573+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
574+ if templ_7745c5c3_Err != nil {
575+ return templ_7745c5c3_Err
576+ }
577+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " <time datetime=\"")
578+ if templ_7745c5c3_Err != nil {
579+ return templ_7745c5c3_Err
580+ }
581+ var templ_7745c5c3_Var30 string
582+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
583+ if templ_7745c5c3_Err != nil {
584+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 30}
585+ }
586+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
587+ if templ_7745c5c3_Err != nil {
588+ return templ_7745c5c3_Err
589+ }
590+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" title=\"")
591+ if templ_7745c5c3_Err != nil {
592+ return templ_7745c5c3_Err
593+ }
594+ var templ_7745c5c3_Var31 string
595+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
596+ if templ_7745c5c3_Err != nil {
597+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 49}
598+ }
599+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
600+ if templ_7745c5c3_Err != nil {
601+ return templ_7745c5c3_Err
602+ }
603+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">")
604+ if templ_7745c5c3_Err != nil {
605+ return templ_7745c5c3_Err
606+ }
607+ var templ_7745c5c3_Var32 string
608+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
609+ if templ_7745c5c3_Err != nil {
610+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 60}
611+ }
612+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
613+ if templ_7745c5c3_Err != nil {
614+ return templ_7745c5c3_Err
615+ }
616+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</time>")
617+ if templ_7745c5c3_Err != nil {
618+ return templ_7745c5c3_Err
619+ }
620+ var templ_7745c5c3_Var33 string
621+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
622+ if templ_7745c5c3_Err != nil {
623+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 77}
624+ }
625+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
626+ if templ_7745c5c3_Err != nil {
627+ return templ_7745c5c3_Err
628+ }
629+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " <a href=\"")
630+ if templ_7745c5c3_Err != nil {
631+ return templ_7745c5c3_Err
632+ }
633+ var templ_7745c5c3_Var34 templ.SafeURL
634+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.AllHref))
635+ if templ_7745c5c3_Err != nil {
636+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 123, Col: 39}
637+ }
638+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
639+ if templ_7745c5c3_Err != nil {
640+ return templ_7745c5c3_Err
641+ }
642+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">all releases</a></dd>")
643+ if templ_7745c5c3_Err != nil {
644+ return templ_7745c5c3_Err
645+ }
646+ }
647+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</dl></section>")
538648 if templ_7745c5c3_Err != nil {
539649 return templ_7745c5c3_Err
540650 }
541651 if len(p.Recent) > 0 {
542- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"")
652+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"")
543653 if templ_7745c5c3_Err != nil {
544654 return templ_7745c5c3_Err
545655 }
546- var templ_7745c5c3_Var27 templ.SafeURL
547- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref))
656+ var templ_7745c5c3_Var35 templ.SafeURL
657+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref))
548658 if templ_7745c5c3_Err != nil {
549- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 105}
659+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 129, Col: 105}
550660 }
551- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
661+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
552662 if templ_7745c5c3_Err != nil {
553663 return templ_7745c5c3_Err
554664 }
555- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\">View all</a></h2>")
665+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">View all</a></h2>")
556666 if templ_7745c5c3_Err != nil {
557667 return templ_7745c5c3_Err
558668 }
@@ -560,7 +670,7 @@ func summaryRail(p SummaryPage) templ.Component {
560670 if templ_7745c5c3_Err != nil {
561671 return templ_7745c5c3_Err
562672 }
563- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</section>")
673+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</section>")
564674 if templ_7745c5c3_Err != nil {
565675 return templ_7745c5c3_Err
566676 }
@@ -572,7 +682,7 @@ func summaryRail(p SummaryPage) templ.Component {
572682 }
573683 }
574684 if len(p.Outline) > 0 && p.Readme != nil {
575- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>")
685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>")
576686 if templ_7745c5c3_Err != nil {
577687 return templ_7745c5c3_Err
578688 }
@@ -580,7 +690,7 @@ func summaryRail(p SummaryPage) templ.Component {
580690 if templ_7745c5c3_Err != nil {
581691 return templ_7745c5c3_Err
582692 }
583- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</section>")
693+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</section>")
584694 if templ_7745c5c3_Err != nil {
585695 return templ_7745c5c3_Err
586696 }
@@ -606,12 +716,12 @@ func Refs(l Layout, p RefsPage) templ.Component {
606716 }()
607717 }
608718 ctx = templ.InitializeContext(ctx)
609- templ_7745c5c3_Var28 := templ.GetChildren(ctx)
610- if templ_7745c5c3_Var28 == nil {
611- templ_7745c5c3_Var28 = templ.NopComponent
719+ templ_7745c5c3_Var36 := templ.GetChildren(ctx)
720+ if templ_7745c5c3_Var36 == nil {
721+ templ_7745c5c3_Var36 = templ.NopComponent
612722 }
613723 ctx = templ.ClearChildren(ctx)
614- templ_7745c5c3_Var29 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
724+ templ_7745c5c3_Var37 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
615725 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
616726 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
617727 if !templ_7745c5c3_IsBuffer {
@@ -623,7 +733,7 @@ func Refs(l Layout, p RefsPage) templ.Component {
623733 }()
624734 }
625735 ctx = templ.InitializeContext(ctx)
626- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<div class=\"filter\"><label for=\"ref-filter\" class=\"sr\">filter</label> <input id=\"ref-filter\" type=\"search\" data-filter=\"#ref-rows > tr\" placeholder=\"Filter branches and tags…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><table class=\"list refs\" aria-label=\"Branches and tags\"><thead><tr><th class=\"c-kind\">kind</th><th class=\"c-name\">name</th><th class=\"c-sha\">commit</th><th class=\"c-sub\">subject</th><th class=\"c-when\">date</th></tr></thead>")
736+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<div class=\"filter\"><label for=\"ref-filter\" class=\"sr\">filter</label> <input id=\"ref-filter\" type=\"search\" data-filter=\"#ref-rows > tr\" placeholder=\"Filter branches and tags…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><table class=\"list refs\" aria-label=\"Branches and tags\"><thead><tr><th class=\"c-kind\">kind</th><th class=\"c-name\">name</th><th class=\"c-sha\">commit</th><th class=\"c-sub\">subject</th><th class=\"c-when\">date</th></tr></thead>")
627737 if templ_7745c5c3_Err != nil {
628738 return templ_7745c5c3_Err
629739 }
@@ -631,32 +741,32 @@ func Refs(l Layout, p RefsPage) templ.Component {
631741 if templ_7745c5c3_Err != nil {
632742 return templ_7745c5c3_Err
633743 }
634- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</table>")
744+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</table>")
635745 if templ_7745c5c3_Err != nil {
636746 return templ_7745c5c3_Err
637747 }
638748 if p.Note != "" {
639- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<p class=\"note\">")
749+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<p class=\"note\">")
640750 if templ_7745c5c3_Err != nil {
641751 return templ_7745c5c3_Err
642752 }
643- var templ_7745c5c3_Var30 string
644- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
753+ var templ_7745c5c3_Var38 string
754+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
645755 if templ_7745c5c3_Err != nil {
646- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 154, Col: 27}
756+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 162, Col: 27}
647757 }
648- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
758+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
649759 if templ_7745c5c3_Err != nil {
650760 return templ_7745c5c3_Err
651761 }
652- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</p>")
762+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</p>")
653763 if templ_7745c5c3_Err != nil {
654764 return templ_7745c5c3_Err
655765 }
656766 }
657767 return nil
658768 })
659- templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var29), templ_7745c5c3_Buffer)
769+ templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var37), templ_7745c5c3_Buffer)
660770 if templ_7745c5c3_Err != nil {
661771 return templ_7745c5c3_Err
662772 }
@@ -680,112 +790,112 @@ func refRow(kind string, r RefRow) templ.Component {
680790 }()
681791 }
682792 ctx = templ.InitializeContext(ctx)
683- templ_7745c5c3_Var31 := templ.GetChildren(ctx)
684- if templ_7745c5c3_Var31 == nil {
685- templ_7745c5c3_Var31 = templ.NopComponent
793+ templ_7745c5c3_Var39 := templ.GetChildren(ctx)
794+ if templ_7745c5c3_Var39 == nil {
795+ templ_7745c5c3_Var39 = templ.NopComponent
686796 }
687797 ctx = templ.ClearChildren(ctx)
688- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<tr")
798+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<tr")
689799 if templ_7745c5c3_Err != nil {
690800 return templ_7745c5c3_Err
691801 }
692802 if r.Cursor {
693- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " data-cursor")
803+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " data-cursor")
694804 if templ_7745c5c3_Err != nil {
695805 return templ_7745c5c3_Err
696806 }
697807 }
698- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "><td class=\"c-kind mute\">")
808+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "><td class=\"c-kind mute\">")
699809 if templ_7745c5c3_Err != nil {
700810 return templ_7745c5c3_Err
701811 }
702- var templ_7745c5c3_Var32 string
703- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(kind)
812+ var templ_7745c5c3_Var40 string
813+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(kind)
704814 if templ_7745c5c3_Err != nil {
705- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 161, Col: 32}
815+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 169, Col: 32}
706816 }
707- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
817+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
708818 if templ_7745c5c3_Err != nil {
709819 return templ_7745c5c3_Err
710820 }
711- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</td><td class=\"c-name\"><a href=\"")
821+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</td><td class=\"c-name\"><a href=\"")
712822 if templ_7745c5c3_Err != nil {
713823 return templ_7745c5c3_Err
714824 }
715- var templ_7745c5c3_Var33 templ.SafeURL
716- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
825+ var templ_7745c5c3_Var41 templ.SafeURL
826+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
717827 if templ_7745c5c3_Err != nil {
718- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 163, Col: 34}
828+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 171, Col: 34}
719829 }
720- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
830+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
721831 if templ_7745c5c3_Err != nil {
722832 return templ_7745c5c3_Err
723833 }
724- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">")
834+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">")
725835 if templ_7745c5c3_Err != nil {
726836 return templ_7745c5c3_Err
727837 }
728- var templ_7745c5c3_Var34 string
729- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
838+ var templ_7745c5c3_Var42 string
839+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
730840 if templ_7745c5c3_Err != nil {
731- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 163, Col: 45}
841+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 171, Col: 45}
732842 }
733- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
843+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
734844 if templ_7745c5c3_Err != nil {
735845 return templ_7745c5c3_Err
736846 }
737- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</a> ")
847+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</a> ")
738848 if templ_7745c5c3_Err != nil {
739849 return templ_7745c5c3_Err
740850 }
741851 if r.Default {
742- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<span class=\"pill\">default</span>")
852+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<span class=\"pill\">default</span>")
743853 if templ_7745c5c3_Err != nil {
744854 return templ_7745c5c3_Err
745855 }
746856 }
747- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</td><td class=\"c-sha\">")
857+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</td><td class=\"c-sha\">")
748858 if templ_7745c5c3_Err != nil {
749859 return templ_7745c5c3_Err
750860 }
751861 if r.CHref != "" {
752- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a href=\"")
862+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a href=\"")
753863 if templ_7745c5c3_Err != nil {
754864 return templ_7745c5c3_Err
755865 }
756- var templ_7745c5c3_Var35 templ.SafeURL
757- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
866+ var templ_7745c5c3_Var43 templ.SafeURL
867+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
758868 if templ_7745c5c3_Err != nil {
759- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 170, Col: 36}
869+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 178, Col: 36}
760870 }
761- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
871+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
762872 if templ_7745c5c3_Err != nil {
763873 return templ_7745c5c3_Err
764874 }
765- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\">")
875+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\">")
766876 if templ_7745c5c3_Err != nil {
767877 return templ_7745c5c3_Err
768878 }
769- var templ_7745c5c3_Var36 string
770- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
879+ var templ_7745c5c3_Var44 string
880+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
771881 if templ_7745c5c3_Err != nil {
772- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 170, Col: 48}
882+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 178, Col: 48}
773883 }
774- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
884+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
775885 if templ_7745c5c3_Err != nil {
776886 return templ_7745c5c3_Err
777887 }
778- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</a>")
888+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</a>")
779889 if templ_7745c5c3_Err != nil {
780890 return templ_7745c5c3_Err
781891 }
782892 } else {
783- var templ_7745c5c3_Var37 string
784- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
893+ var templ_7745c5c3_Var45 string
894+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
785895 if templ_7745c5c3_Err != nil {
786- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 172, Col: 13}
896+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 180, Col: 13}
787897 }
788- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
898+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
789899 if templ_7745c5c3_Err != nil {
790900 return templ_7745c5c3_Err
791901 }
@@ -794,69 +904,69 @@ func refRow(kind string, r RefRow) templ.Component {
794904 if templ_7745c5c3_Err != nil {
795905 return templ_7745c5c3_Err
796906 }
797- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</td><td class=\"c-sub\">")
907+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</td><td class=\"c-sub\">")
798908 if templ_7745c5c3_Err != nil {
799909 return templ_7745c5c3_Err
800910 }
801- var templ_7745c5c3_Var38 string
802- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)
911+ var templ_7745c5c3_Var46 string
912+ templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)
803913 if templ_7745c5c3_Err != nil {
804- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 176, Col: 31}
914+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 184, Col: 31}
805915 }
806- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
916+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
807917 if templ_7745c5c3_Err != nil {
808918 return templ_7745c5c3_Err
809919 }
810- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</td><td class=\"c-when dim\">")
920+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</td><td class=\"c-when dim\">")
811921 if templ_7745c5c3_Err != nil {
812922 return templ_7745c5c3_Err
813923 }
814924 if r.WhenAt != "" {
815- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<time datetime=\"")
925+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<time datetime=\"")
816926 if templ_7745c5c3_Err != nil {
817927 return templ_7745c5c3_Err
818928 }
819- var templ_7745c5c3_Var39 string
820- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
929+ var templ_7745c5c3_Var47 string
930+ templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
821931 if templ_7745c5c3_Err != nil {
822- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 179, Col: 29}
932+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 29}
823933 }
824- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
934+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47)
825935 if templ_7745c5c3_Err != nil {
826936 return templ_7745c5c3_Err
827937 }
828- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" title=\"")
938+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" title=\"")
829939 if templ_7745c5c3_Err != nil {
830940 return templ_7745c5c3_Err
831941 }
832- var templ_7745c5c3_Var40 string
833- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
942+ var templ_7745c5c3_Var48 string
943+ templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
834944 if templ_7745c5c3_Err != nil {
835- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 179, Col: 48}
945+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 48}
836946 }
837- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40)
947+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48)
838948 if templ_7745c5c3_Err != nil {
839949 return templ_7745c5c3_Err
840950 }
841- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\">")
951+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\">")
842952 if templ_7745c5c3_Err != nil {
843953 return templ_7745c5c3_Err
844954 }
845- var templ_7745c5c3_Var41 string
846- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
955+ var templ_7745c5c3_Var49 string
956+ templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
847957 if templ_7745c5c3_Err != nil {
848- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 179, Col: 59}
958+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 59}
849959 }
850- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
960+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
851961 if templ_7745c5c3_Err != nil {
852962 return templ_7745c5c3_Err
853963 }
854- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</time>")
964+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</time>")
855965 if templ_7745c5c3_Err != nil {
856966 return templ_7745c5c3_Err
857967 }
858968 }
859- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</td></tr>")
969+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</td></tr>")
860970 if templ_7745c5c3_Err != nil {
861971 return templ_7745c5c3_Err
862972 }
@@ -880,12 +990,12 @@ func refsRail(p RefsPage) templ.Component {
880990 }()
881991 }
882992 ctx = templ.InitializeContext(ctx)
883- templ_7745c5c3_Var42 := templ.GetChildren(ctx)
884- if templ_7745c5c3_Var42 == nil {
885- templ_7745c5c3_Var42 = templ.NopComponent
993+ templ_7745c5c3_Var50 := templ.GetChildren(ctx)
994+ if templ_7745c5c3_Var50 == nil {
995+ templ_7745c5c3_Var50 = templ.NopComponent
886996 }
887997 ctx = templ.ClearChildren(ctx)
888- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>")
998+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>")
889999 if templ_7745c5c3_Err != nil {
8901000 return templ_7745c5c3_Err
8911001 }
@@ -893,7 +1003,7 @@ func refsRail(p RefsPage) templ.Component {
8931003 if templ_7745c5c3_Err != nil {
8941004 return templ_7745c5c3_Err
8951005 }
896- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</section>")
1006+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</section>")
8971007 if templ_7745c5c3_Err != nil {
8981008 return templ_7745c5c3_Err
8991009 }
@@ -918,12 +1028,12 @@ func State(l Layout, p StatePage) templ.Component {
9181028 }()
9191029 }
9201030 ctx = templ.InitializeContext(ctx)
921- templ_7745c5c3_Var43 := templ.GetChildren(ctx)
922- if templ_7745c5c3_Var43 == nil {
923- templ_7745c5c3_Var43 = templ.NopComponent
1031+ templ_7745c5c3_Var51 := templ.GetChildren(ctx)
1032+ if templ_7745c5c3_Var51 == nil {
1033+ templ_7745c5c3_Var51 = templ.NopComponent
9241034 }
9251035 ctx = templ.ClearChildren(ctx)
926- templ_7745c5c3_Var44 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1036+ templ_7745c5c3_Var52 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
9271037 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
9281038 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
9291039 if !templ_7745c5c3_IsBuffer {
@@ -935,51 +1045,51 @@ func State(l Layout, p StatePage) templ.Component {
9351045 }()
9361046 }
9371047 ctx = templ.InitializeContext(ctx)
938- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">")
1048+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">")
9391049 if templ_7745c5c3_Err != nil {
9401050 return templ_7745c5c3_Err
9411051 }
942- var templ_7745c5c3_Var45 string
943- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code)
1052+ var templ_7745c5c3_Var53 string
1053+ templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code)
9441054 if templ_7745c5c3_Err != nil {
945- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 196, Col: 30}
1055+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 204, Col: 30}
9461056 }
947- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
1057+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
9481058 if templ_7745c5c3_Err != nil {
9491059 return templ_7745c5c3_Err
9501060 }
951- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</p><h1 id=\"state-title\">")
1061+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</p><h1 id=\"state-title\">")
9521062 if templ_7745c5c3_Err != nil {
9531063 return templ_7745c5c3_Err
9541064 }
955- var templ_7745c5c3_Var46 string
956- templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
1065+ var templ_7745c5c3_Var54 string
1066+ templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
9571067 if templ_7745c5c3_Err != nil {
958- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 197, Col: 33}
1068+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 205, Col: 33}
9591069 }
960- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
1070+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
9611071 if templ_7745c5c3_Err != nil {
9621072 return templ_7745c5c3_Err
9631073 }
964- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</h1>")
1074+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</h1>")
9651075 if templ_7745c5c3_Err != nil {
9661076 return templ_7745c5c3_Err
9671077 }
9681078 for _, line := range p.Lines {
969- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<p class=\"dim\">")
1079+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<p class=\"dim\">")
9701080 if templ_7745c5c3_Err != nil {
9711081 return templ_7745c5c3_Err
9721082 }
973- var templ_7745c5c3_Var47 string
974- templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(line)
1083+ var templ_7745c5c3_Var55 string
1084+ templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(line)
9751085 if templ_7745c5c3_Err != nil {
976- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 199, Col: 25}
1086+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 207, Col: 25}
9771087 }
978- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
1088+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
9791089 if templ_7745c5c3_Err != nil {
9801090 return templ_7745c5c3_Err
9811091 }
982- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</p>")
1092+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</p>")
9831093 if templ_7745c5c3_Err != nil {
9841094 return templ_7745c5c3_Err
9851095 }
@@ -990,49 +1100,49 @@ func State(l Layout, p StatePage) templ.Component {
9901100 return templ_7745c5c3_Err
9911101 }
9921102 }
993- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<p class=\"state-back\">")
1103+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<p class=\"state-back\">")
9941104 if templ_7745c5c3_Err != nil {
9951105 return templ_7745c5c3_Err
9961106 }
9971107 for _, b := range p.Back {
998- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<a href=\"")
1108+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<a href=\"")
9991109 if templ_7745c5c3_Err != nil {
10001110 return templ_7745c5c3_Err
10011111 }
1002- var templ_7745c5c3_Var48 templ.SafeURL
1003- templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href))
1112+ var templ_7745c5c3_Var56 templ.SafeURL
1113+ templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href))
10041114 if templ_7745c5c3_Err != nil {
1005- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 206, Col: 36}
1115+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 214, Col: 36}
10061116 }
1007- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
1117+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
10081118 if templ_7745c5c3_Err != nil {
10091119 return templ_7745c5c3_Err
10101120 }
1011- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\">")
1121+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "\">")
10121122 if templ_7745c5c3_Err != nil {
10131123 return templ_7745c5c3_Err
10141124 }
1015- var templ_7745c5c3_Var49 string
1016- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label)
1125+ var templ_7745c5c3_Var57 string
1126+ templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label)
10171127 if templ_7745c5c3_Err != nil {
1018- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 206, Col: 48}
1128+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 214, Col: 48}
10191129 }
1020- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
1130+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
10211131 if templ_7745c5c3_Err != nil {
10221132 return templ_7745c5c3_Err
10231133 }
1024- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</a>")
1134+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</a>")
10251135 if templ_7745c5c3_Err != nil {
10261136 return templ_7745c5c3_Err
10271137 }
10281138 }
1029- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</p></section>")
1139+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</p></section>")
10301140 if templ_7745c5c3_Err != nil {
10311141 return templ_7745c5c3_Err
10321142 }
10331143 return nil
10341144 })
1035- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var44), templ_7745c5c3_Buffer)
1145+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var52), templ_7745c5c3_Buffer)
10361146 if templ_7745c5c3_Err != nil {
10371147 return templ_7745c5c3_Err
10381148 }
@@ -1061,12 +1171,12 @@ func Login(l Layout, p LoginPage) templ.Component {
10611171 }()
10621172 }
10631173 ctx = templ.InitializeContext(ctx)
1064- templ_7745c5c3_Var50 := templ.GetChildren(ctx)
1065- if templ_7745c5c3_Var50 == nil {
1066- templ_7745c5c3_Var50 = templ.NopComponent
1174+ templ_7745c5c3_Var58 := templ.GetChildren(ctx)
1175+ if templ_7745c5c3_Var58 == nil {
1176+ templ_7745c5c3_Var58 = templ.NopComponent
10671177 }
10681178 ctx = templ.ClearChildren(ctx)
1069- templ_7745c5c3_Var51 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1179+ templ_7745c5c3_Var59 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
10701180 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
10711181 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
10721182 if !templ_7745c5c3_IsBuffer {
@@ -1078,75 +1188,75 @@ func Login(l Layout, p LoginPage) templ.Component {
10781188 }()
10791189 }
10801190 ctx = templ.InitializeContext(ctx)
1081- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
1191+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
10821192 if templ_7745c5c3_Err != nil {
10831193 return templ_7745c5c3_Err
10841194 }
1085- var templ_7745c5c3_Var52 string
1086- templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
1195+ var templ_7745c5c3_Var60 string
1196+ templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
10871197 if templ_7745c5c3_Err != nil {
1088- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 221, Col: 33}
1198+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 229, Col: 33}
10891199 }
1090- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
1200+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60))
10911201 if templ_7745c5c3_Err != nil {
10921202 return templ_7745c5c3_Err
10931203 }
1094- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</h1>")
1204+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</h1>")
10951205 if templ_7745c5c3_Err != nil {
10961206 return templ_7745c5c3_Err
10971207 }
10981208 for _, line := range p.Lines {
1099- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<p class=\"dim\">")
1209+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<p class=\"dim\">")
11001210 if templ_7745c5c3_Err != nil {
11011211 return templ_7745c5c3_Err
11021212 }
1103- var templ_7745c5c3_Var53 string
1104- templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(line)
1213+ var templ_7745c5c3_Var61 string
1214+ templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(line)
11051215 if templ_7745c5c3_Err != nil {
1106- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 223, Col: 25}
1216+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 231, Col: 25}
11071217 }
1108- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
1218+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
11091219 if templ_7745c5c3_Err != nil {
11101220 return templ_7745c5c3_Err
11111221 }
1112- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</p>")
1222+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</p>")
11131223 if templ_7745c5c3_Err != nil {
11141224 return templ_7745c5c3_Err
11151225 }
11161226 }
1117- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<form class=\"state-act\" method=\"post\" action=\"")
1227+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "<form class=\"state-act\" method=\"post\" action=\"")
11181228 if templ_7745c5c3_Err != nil {
11191229 return templ_7745c5c3_Err
11201230 }
1121- var templ_7745c5c3_Var54 templ.SafeURL
1122- templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))
1231+ var templ_7745c5c3_Var62 templ.SafeURL
1232+ templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))
11231233 if templ_7745c5c3_Err != nil {
1124- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 225, Col: 73}
1234+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 233, Col: 73}
11251235 }
1126- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
1236+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
11271237 if templ_7745c5c3_Err != nil {
11281238 return templ_7745c5c3_Err
11291239 }
1130- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">")
1240+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">")
11311241 if templ_7745c5c3_Err != nil {
11321242 return templ_7745c5c3_Err
11331243 }
1134- var templ_7745c5c3_Var55 string
1135- templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
1244+ var templ_7745c5c3_Var63 string
1245+ templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
11361246 if templ_7745c5c3_Err != nil {
1137- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 226, Col: 56}
1247+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 234, Col: 56}
11381248 }
1139- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
1249+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
11401250 if templ_7745c5c3_Err != nil {
11411251 return templ_7745c5c3_Err
11421252 }
1143- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
1253+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
11441254 if templ_7745c5c3_Err != nil {
11451255 return templ_7745c5c3_Err
11461256 }
11471257 return nil
11481258 })
1149- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var51), templ_7745c5c3_Buffer)
1259+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var59), templ_7745c5c3_Buffer)
11501260 if templ_7745c5c3_Err != nil {
11511261 return templ_7745c5c3_Err
11521262 }
@@ -1172,12 +1282,12 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component {
11721282 }()
11731283 }
11741284 ctx = templ.InitializeContext(ctx)
1175- templ_7745c5c3_Var56 := templ.GetChildren(ctx)
1176- if templ_7745c5c3_Var56 == nil {
1177- templ_7745c5c3_Var56 = templ.NopComponent
1285+ templ_7745c5c3_Var64 := templ.GetChildren(ctx)
1286+ if templ_7745c5c3_Var64 == nil {
1287+ templ_7745c5c3_Var64 = templ.NopComponent
11781288 }
11791289 ctx = templ.ClearChildren(ctx)
1180- templ_7745c5c3_Var57 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1290+ templ_7745c5c3_Var65 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
11811291 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
11821292 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
11831293 if !templ_7745c5c3_IsBuffer {
@@ -1189,88 +1299,88 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component {
11891299 }()
11901300 }
11911301 ctx = templ.InitializeContext(ctx)
1192- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
1302+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
11931303 if templ_7745c5c3_Err != nil {
11941304 return templ_7745c5c3_Err
11951305 }
1196- var templ_7745c5c3_Var58 string
1197- templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
1306+ var templ_7745c5c3_Var66 string
1307+ templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
11981308 if templ_7745c5c3_Err != nil {
1199- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 238, Col: 33}
1309+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 246, Col: 33}
12001310 }
1201- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
1311+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))
12021312 if templ_7745c5c3_Err != nil {
12031313 return templ_7745c5c3_Err
12041314 }
1205- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</h1><form class=\"state-act\" data-js data-passkey-form><button type=\"submit\" class=\"btn primary\" data-passkey=\"login\" data-begin=\"")
1315+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "</h1><form class=\"state-act\" data-js data-passkey-form><button type=\"submit\" class=\"btn primary\" data-passkey=\"login\" data-begin=\"")
12061316 if templ_7745c5c3_Err != nil {
12071317 return templ_7745c5c3_Err
12081318 }
1209- var templ_7745c5c3_Var59 string
1210- templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
1319+ var templ_7745c5c3_Var67 string
1320+ templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
12111321 if templ_7745c5c3_Err != nil {
1212- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 240, Col: 87}
1322+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 87}
12131323 }
1214- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59)
1324+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
12151325 if templ_7745c5c3_Err != nil {
12161326 return templ_7745c5c3_Err
12171327 }
1218- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\" data-finish=\"")
1328+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "\" data-finish=\"")
12191329 if templ_7745c5c3_Err != nil {
12201330 return templ_7745c5c3_Err
12211331 }
1222- var templ_7745c5c3_Var60 string
1223- templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
1332+ var templ_7745c5c3_Var68 string
1333+ templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
12241334 if templ_7745c5c3_Err != nil {
1225- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 240, Col: 112}
1335+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 112}
12261336 }
1227- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)
1337+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var68)
12281338 if templ_7745c5c3_Err != nil {
12291339 return templ_7745c5c3_Err
12301340 }
1231- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "\">")
1341+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "\">")
12321342 if templ_7745c5c3_Err != nil {
12331343 return templ_7745c5c3_Err
12341344 }
1235- var templ_7745c5c3_Var61 string
1236- templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
1345+ var templ_7745c5c3_Var69 string
1346+ templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
12371347 if templ_7745c5c3_Err != nil {
1238- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 240, Col: 125}
1348+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 125}
12391349 }
1240- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
1350+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
12411351 if templ_7745c5c3_Err != nil {
12421352 return templ_7745c5c3_Err
12431353 }
1244- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
1354+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
12451355 if templ_7745c5c3_Err != nil {
12461356 return templ_7745c5c3_Err
12471357 }
12481358 for _, line := range p.Lines {
1249- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<p class=\"dim\">")
1359+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<p class=\"dim\">")
12501360 if templ_7745c5c3_Err != nil {
12511361 return templ_7745c5c3_Err
12521362 }
1253- var templ_7745c5c3_Var62 string
1254- templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(line)
1363+ var templ_7745c5c3_Var70 string
1364+ templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(line)
12551365 if templ_7745c5c3_Err != nil {
1256- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 244, Col: 25}
1366+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 252, Col: 25}
12571367 }
1258- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
1368+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
12591369 if templ_7745c5c3_Err != nil {
12601370 return templ_7745c5c3_Err
12611371 }
1262- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</p>")
1372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "</p>")
12631373 if templ_7745c5c3_Err != nil {
12641374 return templ_7745c5c3_Err
12651375 }
12661376 }
1267- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
1377+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
12681378 if templ_7745c5c3_Err != nil {
12691379 return templ_7745c5c3_Err
12701380 }
12711381 return nil
12721382 })
1273- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var57), templ_7745c5c3_Buffer)
1383+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var65), templ_7745c5c3_Buffer)
12741384 if templ_7745c5c3_Err != nil {
12751385 return templ_7745c5c3_Err
12761386 }
@@ -1295,12 +1405,12 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component {
12951405 }()
12961406 }
12971407 ctx = templ.InitializeContext(ctx)
1298- templ_7745c5c3_Var63 := templ.GetChildren(ctx)
1299- if templ_7745c5c3_Var63 == nil {
1300- templ_7745c5c3_Var63 = templ.NopComponent
1408+ templ_7745c5c3_Var71 := templ.GetChildren(ctx)
1409+ if templ_7745c5c3_Var71 == nil {
1410+ templ_7745c5c3_Var71 = templ.NopComponent
13011411 }
13021412 ctx = templ.ClearChildren(ctx)
1303- templ_7745c5c3_Var64 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1413+ templ_7745c5c3_Var72 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
13041414 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
13051415 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
13061416 if !templ_7745c5c3_IsBuffer {
@@ -1312,231 +1422,231 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component {
13121422 }()
13131423 }
13141424 ctx = templ.InitializeContext(ctx)
1315- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">")
1425+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">")
13161426 if templ_7745c5c3_Err != nil {
13171427 return templ_7745c5c3_Err
13181428 }
1319- var templ_7745c5c3_Var65 string
1320- templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))
1429+ var templ_7745c5c3_Var73 string
1430+ templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))
13211431 if templ_7745c5c3_Err != nil {
1322- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 254, Col: 81}
1432+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 262, Col: 81}
13231433 }
1324- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
1434+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
13251435 if templ_7745c5c3_Err != nil {
13261436 return templ_7745c5c3_Err
13271437 }
1328- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "</span></h2><p class=\"dim\">A passkey signs you in from this device or your phone, without a terminal. Removing one signs every browser out.</p>")
1438+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</span></h2><p class=\"dim\">A passkey signs you in from this device or your phone, without a terminal. Removing one signs every browser out.</p>")
13291439 if templ_7745c5c3_Err != nil {
13301440 return templ_7745c5c3_Err
13311441 }
13321442 if len(p.Rows) > 0 {
1333- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "<table class=\"list passkeys\" aria-label=\"Passkeys\"><thead><tr><th class=\"c-name\">Name</th><th class=\"c-when\">Created</th><th class=\"c-when\">Last used</th><th class=\"c-sync\">Synced</th><th class=\"c-act\"><span class=\"sr\">Remove</span></th></tr></thead> <tbody>")
1443+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<table class=\"list passkeys\" aria-label=\"Passkeys\"><thead><tr><th class=\"c-name\">Name</th><th class=\"c-when\">Created</th><th class=\"c-when\">Last used</th><th class=\"c-sync\">Synced</th><th class=\"c-act\"><span class=\"sr\">Remove</span></th></tr></thead> <tbody>")
13341444 if templ_7745c5c3_Err != nil {
13351445 return templ_7745c5c3_Err
13361446 }
13371447 for _, r := range p.Rows {
1338- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "<tr><td class=\"c-name\">")
1448+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "<tr><td class=\"c-name\">")
13391449 if templ_7745c5c3_Err != nil {
13401450 return templ_7745c5c3_Err
13411451 }
1342- var templ_7745c5c3_Var66 string
1343- templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
1452+ var templ_7745c5c3_Var74 string
1453+ templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
13441454 if templ_7745c5c3_Err != nil {
1345- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 270, Col: 34}
1455+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 278, Col: 34}
13461456 }
1347- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))
1457+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74))
13481458 if templ_7745c5c3_Err != nil {
13491459 return templ_7745c5c3_Err
13501460 }
1351- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</td><td class=\"c-when nowrap\"><time")
1461+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</td><td class=\"c-when nowrap\"><time")
13521462 if templ_7745c5c3_Err != nil {
13531463 return templ_7745c5c3_Err
13541464 }
13551465 if r.CreatedAt != "" {
1356- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, " datetime=\"")
1466+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, " datetime=\"")
13571467 if templ_7745c5c3_Err != nil {
13581468 return templ_7745c5c3_Err
13591469 }
1360- var templ_7745c5c3_Var67 string
1361- templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt)
1470+ var templ_7745c5c3_Var75 string
1471+ templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt)
13621472 if templ_7745c5c3_Err != nil {
1363- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 274, Col: 32}
1473+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 282, Col: 32}
13641474 }
1365- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
1475+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75)
13661476 if templ_7745c5c3_Err != nil {
13671477 return templ_7745c5c3_Err
13681478 }
1369- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "\"")
1479+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"")
13701480 if templ_7745c5c3_Err != nil {
13711481 return templ_7745c5c3_Err
13721482 }
13731483 }
1374- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, ">")
1484+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, ">")
13751485 if templ_7745c5c3_Err != nil {
13761486 return templ_7745c5c3_Err
13771487 }
1378- var templ_7745c5c3_Var68 string
1379- templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created)
1488+ var templ_7745c5c3_Var76 string
1489+ templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created)
13801490 if templ_7745c5c3_Err != nil {
1381- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 276, Col: 20}
1491+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 284, Col: 20}
13821492 }
1383- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
1493+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
13841494 if templ_7745c5c3_Err != nil {
13851495 return templ_7745c5c3_Err
13861496 }
1387- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "</time></td><td class=\"c-when nowrap\"><time")
1497+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "</time></td><td class=\"c-when nowrap\"><time")
13881498 if templ_7745c5c3_Err != nil {
13891499 return templ_7745c5c3_Err
13901500 }
13911501 if r.LastUsedAt != "" {
1392- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " datetime=\"")
1502+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, " datetime=\"")
13931503 if templ_7745c5c3_Err != nil {
13941504 return templ_7745c5c3_Err
13951505 }
1396- var templ_7745c5c3_Var69 string
1397- templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt)
1506+ var templ_7745c5c3_Var77 string
1507+ templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt)
13981508 if templ_7745c5c3_Err != nil {
1399- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 281, Col: 33}
1509+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 289, Col: 33}
14001510 }
1401- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69)
1511+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77)
14021512 if templ_7745c5c3_Err != nil {
14031513 return templ_7745c5c3_Err
14041514 }
1405- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "\"")
1515+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\"")
14061516 if templ_7745c5c3_Err != nil {
14071517 return templ_7745c5c3_Err
14081518 }
14091519 }
1410- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, ">")
1520+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, ">")
14111521 if templ_7745c5c3_Err != nil {
14121522 return templ_7745c5c3_Err
14131523 }
1414- var templ_7745c5c3_Var70 string
1415- templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed)
1524+ var templ_7745c5c3_Var78 string
1525+ templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed)
14161526 if templ_7745c5c3_Err != nil {
1417- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 283, Col: 21}
1527+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 291, Col: 21}
14181528 }
1419- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
1529+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78))
14201530 if templ_7745c5c3_Err != nil {
14211531 return templ_7745c5c3_Err
14221532 }
1423- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</time></td><td class=\"c-sync\">")
1533+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</time></td><td class=\"c-sync\">")
14241534 if templ_7745c5c3_Err != nil {
14251535 return templ_7745c5c3_Err
14261536 }
14271537 if r.Synced {
1428- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "yes")
1538+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "yes")
14291539 if templ_7745c5c3_Err != nil {
14301540 return templ_7745c5c3_Err
14311541 }
14321542 } else {
1433- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "no")
1543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "no")
14341544 if templ_7745c5c3_Err != nil {
14351545 return templ_7745c5c3_Err
14361546 }
14371547 }
1438- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</td><td class=\"c-act\"><form method=\"post\" action=\"")
1548+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</td><td class=\"c-act\"><form method=\"post\" action=\"")
14391549 if templ_7745c5c3_Err != nil {
14401550 return templ_7745c5c3_Err
14411551 }
1442- var templ_7745c5c3_Var71 templ.SafeURL
1443- templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove))
1552+ var templ_7745c5c3_Var79 templ.SafeURL
1553+ templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove))
14441554 if templ_7745c5c3_Err != nil {
1445- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 293, Col: 60}
1555+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 301, Col: 60}
14461556 }
1447- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
1557+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79))
14481558 if templ_7745c5c3_Err != nil {
14491559 return templ_7745c5c3_Err
14501560 }
1451- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"")
1561+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"")
14521562 if templ_7745c5c3_Err != nil {
14531563 return templ_7745c5c3_Err
14541564 }
1455- var templ_7745c5c3_Var72 string
1456- templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name)
1565+ var templ_7745c5c3_Var80 string
1566+ templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name)
14571567 if templ_7745c5c3_Err != nil {
1458- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 294, Col: 89}
1568+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 302, Col: 89}
14591569 }
1460- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
1570+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)
14611571 if templ_7745c5c3_Err != nil {
14621572 return templ_7745c5c3_Err
14631573 }
1464- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "\">Remove</button></form></td></tr>")
1574+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "\">Remove</button></form></td></tr>")
14651575 if templ_7745c5c3_Err != nil {
14661576 return templ_7745c5c3_Err
14671577 }
14681578 }
1469- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</tbody></table>")
1579+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</tbody></table>")
14701580 if templ_7745c5c3_Err != nil {
14711581 return templ_7745c5c3_Err
14721582 }
14731583 } else {
1474- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<p class=\"lede dim\">no passkeys yet.</p>")
1584+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<p class=\"lede dim\">no passkeys yet.</p>")
14751585 if templ_7745c5c3_Err != nil {
14761586 return templ_7745c5c3_Err
14771587 }
14781588 }
1479- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, " ")
1589+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, " ")
14801590 if templ_7745c5c3_Err != nil {
14811591 return templ_7745c5c3_Err
14821592 }
14831593 if p.Full {
1484- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "<p class=\"dim\">")
1594+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<p class=\"dim\">")
14851595 if templ_7745c5c3_Err != nil {
14861596 return templ_7745c5c3_Err
14871597 }
1488- var templ_7745c5c3_Var73 string
1489- templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max))
1598+ var templ_7745c5c3_Var81 string
1599+ templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max))
14901600 if templ_7745c5c3_Err != nil {
1491- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 305, Col: 31}
1601+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 313, Col: 31}
14921602 }
1493- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
1603+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81))
14941604 if templ_7745c5c3_Err != nil {
14951605 return templ_7745c5c3_Err
14961606 }
1497- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, " passkeys are registered, the most there can be. Remove one to add another.</p>")
1607+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, " passkeys are registered, the most there can be. Remove one to add another.</p>")
14981608 if templ_7745c5c3_Err != nil {
14991609 return templ_7745c5c3_Err
15001610 }
15011611 } else {
1502- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<form class=\"passkey-add\" data-js data-passkey-form><label for=\"passkey-name\" class=\"sr\">Name of the new passkey</label> <input id=\"passkey-name\" type=\"text\" placeholder=\"Name, for example Pixel 9\" autocomplete=\"off\" data-passkey-name> <button type=\"submit\" class=\"btn primary\" data-passkey=\"register\" data-begin=\"")
1612+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<form class=\"passkey-add\" data-js data-passkey-form><label for=\"passkey-name\" class=\"sr\">Name of the new passkey</label> <input id=\"passkey-name\" type=\"text\" placeholder=\"Name, for example Pixel 9\" autocomplete=\"off\" data-passkey-name> <button type=\"submit\" class=\"btn primary\" data-passkey=\"register\" data-begin=\"")
15031613 if templ_7745c5c3_Err != nil {
15041614 return templ_7745c5c3_Err
15051615 }
1506- var templ_7745c5c3_Var74 string
1507- templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
1616+ var templ_7745c5c3_Var82 string
1617+ templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
15081618 if templ_7745c5c3_Err != nil {
1509- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 310, Col: 90}
1619+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 318, Col: 90}
15101620 }
1511- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74)
1621+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82)
15121622 if templ_7745c5c3_Err != nil {
15131623 return templ_7745c5c3_Err
15141624 }
1515- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "\" data-finish=\"")
1625+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "\" data-finish=\"")
15161626 if templ_7745c5c3_Err != nil {
15171627 return templ_7745c5c3_Err
15181628 }
1519- var templ_7745c5c3_Var75 string
1520- templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
1629+ var templ_7745c5c3_Var83 string
1630+ templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
15211631 if templ_7745c5c3_Err != nil {
1522- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 310, Col: 115}
1632+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 318, Col: 115}
15231633 }
1524- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75)
1634+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83)
15251635 if templ_7745c5c3_Err != nil {
15261636 return templ_7745c5c3_Err
15271637 }
1528- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\">Add a passkey</button></form>")
1638+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "\">Add a passkey</button></form>")
15291639 if templ_7745c5c3_Err != nil {
15301640 return templ_7745c5c3_Err
15311641 }
15321642 }
1533- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
1643+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
15341644 if templ_7745c5c3_Err != nil {
15351645 return templ_7745c5c3_Err
15361646 }
15371647 return nil
15381648 })
1539- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var64), templ_7745c5c3_Buffer)
1649+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var72), templ_7745c5c3_Buffer)
15401650 if templ_7745c5c3_Err != nil {
15411651 return templ_7745c5c3_Err
15421652 }
@@ -534,25 +534,135 @@ func summaryRail(p SummaryPage) templ.Component {
534 return templ_7745c5c3_Err534 return templ_7745c5c3_Err
535 }535 }
536 }536 }
537- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</dl></section>")537+ if r := p.Release; r != nil {
538+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<dt>release</dt><dd><a href=\"")
539+ if templ_7745c5c3_Err != nil {
540+ return templ_7745c5c3_Err
541+ }
542+ var templ_7745c5c3_Var27 templ.SafeURL
543+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
544+ if templ_7745c5c3_Err != nil {
545+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 36}
546+ }
547+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
548+ if templ_7745c5c3_Err != nil {
549+ return templ_7745c5c3_Err
550+ }
551+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\">")
552+ if templ_7745c5c3_Err != nil {
553+ return templ_7745c5c3_Err
554+ }
555+ var templ_7745c5c3_Var28 string
556+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(r.Tag)
557+ if templ_7745c5c3_Err != nil {
558+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 46}
559+ }
560+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
561+ if templ_7745c5c3_Err != nil {
562+ return templ_7745c5c3_Err
563+ }
564+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</a>")
565+ if templ_7745c5c3_Err != nil {
566+ return templ_7745c5c3_Err
567+ }
568+ var templ_7745c5c3_Var29 string
569+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
570+ if templ_7745c5c3_Err != nil {
571+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 60}
572+ }
573+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
574+ if templ_7745c5c3_Err != nil {
575+ return templ_7745c5c3_Err
576+ }
577+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " <time datetime=\"")
578+ if templ_7745c5c3_Err != nil {
579+ return templ_7745c5c3_Err
580+ }
581+ var templ_7745c5c3_Var30 string
582+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
583+ if templ_7745c5c3_Err != nil {
584+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 30}
585+ }
586+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
587+ if templ_7745c5c3_Err != nil {
588+ return templ_7745c5c3_Err
589+ }
590+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" title=\"")
591+ if templ_7745c5c3_Err != nil {
592+ return templ_7745c5c3_Err
593+ }
594+ var templ_7745c5c3_Var31 string
595+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
596+ if templ_7745c5c3_Err != nil {
597+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 49}
598+ }
599+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
600+ if templ_7745c5c3_Err != nil {
601+ return templ_7745c5c3_Err
602+ }
603+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">")
604+ if templ_7745c5c3_Err != nil {
605+ return templ_7745c5c3_Err
606+ }
607+ var templ_7745c5c3_Var32 string
608+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
609+ if templ_7745c5c3_Err != nil {
610+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 60}
611+ }
612+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
613+ if templ_7745c5c3_Err != nil {
614+ return templ_7745c5c3_Err
615+ }
616+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</time>")
617+ if templ_7745c5c3_Err != nil {
618+ return templ_7745c5c3_Err
619+ }
620+ var templ_7745c5c3_Var33 string
621+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
622+ if templ_7745c5c3_Err != nil {
623+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 77}
624+ }
625+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
626+ if templ_7745c5c3_Err != nil {
627+ return templ_7745c5c3_Err
628+ }
629+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " <a href=\"")
630+ if templ_7745c5c3_Err != nil {
631+ return templ_7745c5c3_Err
632+ }
633+ var templ_7745c5c3_Var34 templ.SafeURL
634+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.AllHref))
635+ if templ_7745c5c3_Err != nil {
636+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 123, Col: 39}
637+ }
638+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
639+ if templ_7745c5c3_Err != nil {
640+ return templ_7745c5c3_Err
641+ }
642+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">all releases</a></dd>")
643+ if templ_7745c5c3_Err != nil {
644+ return templ_7745c5c3_Err
645+ }
646+ }
647+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</dl></section>")
538 if templ_7745c5c3_Err != nil {648 if templ_7745c5c3_Err != nil {
539 return templ_7745c5c3_Err649 return templ_7745c5c3_Err
540 }650 }
541 if len(p.Recent) > 0 {651 if len(p.Recent) > 0 {
542- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"")652+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"")
543 if templ_7745c5c3_Err != nil {653 if templ_7745c5c3_Err != nil {
544 return templ_7745c5c3_Err654 return templ_7745c5c3_Err
545 }655 }
546- var templ_7745c5c3_Var27 templ.SafeURL656+ var templ_7745c5c3_Var35 templ.SafeURL
547- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref))657+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref))
548 if templ_7745c5c3_Err != nil {658 if templ_7745c5c3_Err != nil {
549- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 105}659+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 129, Col: 105}
550 }660 }
551- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))661+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
552 if templ_7745c5c3_Err != nil {662 if templ_7745c5c3_Err != nil {
553 return templ_7745c5c3_Err663 return templ_7745c5c3_Err
554 }664 }
555- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\">View all</a></h2>")665+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">View all</a></h2>")
556 if templ_7745c5c3_Err != nil {666 if templ_7745c5c3_Err != nil {
557 return templ_7745c5c3_Err667 return templ_7745c5c3_Err
558 }668 }
@@ -560,7 +670,7 @@ func summaryRail(p SummaryPage) templ.Component {
560 if templ_7745c5c3_Err != nil {670 if templ_7745c5c3_Err != nil {
561 return templ_7745c5c3_Err671 return templ_7745c5c3_Err
562 }672 }
563- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</section>")673+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</section>")
564 if templ_7745c5c3_Err != nil {674 if templ_7745c5c3_Err != nil {
565 return templ_7745c5c3_Err675 return templ_7745c5c3_Err
566 }676 }
@@ -572,7 +682,7 @@ func summaryRail(p SummaryPage) templ.Component {
572 }682 }
573 }683 }
574 if len(p.Outline) > 0 && p.Readme != nil {684 if len(p.Outline) > 0 && p.Readme != nil {
575- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>")685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>")
576 if templ_7745c5c3_Err != nil {686 if templ_7745c5c3_Err != nil {
577 return templ_7745c5c3_Err687 return templ_7745c5c3_Err
578 }688 }
@@ -580,7 +690,7 @@ func summaryRail(p SummaryPage) templ.Component {
580 if templ_7745c5c3_Err != nil {690 if templ_7745c5c3_Err != nil {
581 return templ_7745c5c3_Err691 return templ_7745c5c3_Err
582 }692 }
583- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</section>")693+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</section>")
584 if templ_7745c5c3_Err != nil {694 if templ_7745c5c3_Err != nil {
585 return templ_7745c5c3_Err695 return templ_7745c5c3_Err
586 }696 }
@@ -606,12 +716,12 @@ func Refs(l Layout, p RefsPage) templ.Component {
606 }()716 }()
607 }717 }
608 ctx = templ.InitializeContext(ctx)718 ctx = templ.InitializeContext(ctx)
609- templ_7745c5c3_Var28 := templ.GetChildren(ctx)719+ templ_7745c5c3_Var36 := templ.GetChildren(ctx)
610- if templ_7745c5c3_Var28 == nil {720+ if templ_7745c5c3_Var36 == nil {
611- templ_7745c5c3_Var28 = templ.NopComponent721+ templ_7745c5c3_Var36 = templ.NopComponent
612 }722 }
613 ctx = templ.ClearChildren(ctx)723 ctx = templ.ClearChildren(ctx)
614- templ_7745c5c3_Var29 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {724+ templ_7745c5c3_Var37 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
615 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context725 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
616 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)726 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
617 if !templ_7745c5c3_IsBuffer {727 if !templ_7745c5c3_IsBuffer {
@@ -623,7 +733,7 @@ func Refs(l Layout, p RefsPage) templ.Component {
623 }()733 }()
624 }734 }
625 ctx = templ.InitializeContext(ctx)735 ctx = templ.InitializeContext(ctx)
626- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<div class=\"filter\"><label for=\"ref-filter\" class=\"sr\">filter</label> <input id=\"ref-filter\" type=\"search\" data-filter=\"#ref-rows > tr\" placeholder=\"Filter branches and tags…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><table class=\"list refs\" aria-label=\"Branches and tags\"><thead><tr><th class=\"c-kind\">kind</th><th class=\"c-name\">name</th><th class=\"c-sha\">commit</th><th class=\"c-sub\">subject</th><th class=\"c-when\">date</th></tr></thead>")736+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<div class=\"filter\"><label for=\"ref-filter\" class=\"sr\">filter</label> <input id=\"ref-filter\" type=\"search\" data-filter=\"#ref-rows > tr\" placeholder=\"Filter branches and tags…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><table class=\"list refs\" aria-label=\"Branches and tags\"><thead><tr><th class=\"c-kind\">kind</th><th class=\"c-name\">name</th><th class=\"c-sha\">commit</th><th class=\"c-sub\">subject</th><th class=\"c-when\">date</th></tr></thead>")
627 if templ_7745c5c3_Err != nil {737 if templ_7745c5c3_Err != nil {
628 return templ_7745c5c3_Err738 return templ_7745c5c3_Err
629 }739 }
@@ -631,32 +741,32 @@ func Refs(l Layout, p RefsPage) templ.Component {
631 if templ_7745c5c3_Err != nil {741 if templ_7745c5c3_Err != nil {
632 return templ_7745c5c3_Err742 return templ_7745c5c3_Err
633 }743 }
634- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</table>")744+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</table>")
635 if templ_7745c5c3_Err != nil {745 if templ_7745c5c3_Err != nil {
636 return templ_7745c5c3_Err746 return templ_7745c5c3_Err
637 }747 }
638 if p.Note != "" {748 if p.Note != "" {
639- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<p class=\"note\">")749+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<p class=\"note\">")
640 if templ_7745c5c3_Err != nil {750 if templ_7745c5c3_Err != nil {
641 return templ_7745c5c3_Err751 return templ_7745c5c3_Err
642 }752 }
643- var templ_7745c5c3_Var30 string753+ var templ_7745c5c3_Var38 string
644- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)754+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
645 if templ_7745c5c3_Err != nil {755 if templ_7745c5c3_Err != nil {
646- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 154, Col: 27}756+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 162, Col: 27}
647 }757 }
648- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))758+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
649 if templ_7745c5c3_Err != nil {759 if templ_7745c5c3_Err != nil {
650 return templ_7745c5c3_Err760 return templ_7745c5c3_Err
651 }761 }
652- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</p>")762+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</p>")
653 if templ_7745c5c3_Err != nil {763 if templ_7745c5c3_Err != nil {
654 return templ_7745c5c3_Err764 return templ_7745c5c3_Err
655 }765 }
656 }766 }
657 return nil767 return nil
658 })768 })
659- templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var29), templ_7745c5c3_Buffer)769+ templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var37), templ_7745c5c3_Buffer)
660 if templ_7745c5c3_Err != nil {770 if templ_7745c5c3_Err != nil {
661 return templ_7745c5c3_Err771 return templ_7745c5c3_Err
662 }772 }
@@ -680,112 +790,112 @@ func refRow(kind string, r RefRow) templ.Component {
680 }()790 }()
681 }791 }
682 ctx = templ.InitializeContext(ctx)792 ctx = templ.InitializeContext(ctx)
683- templ_7745c5c3_Var31 := templ.GetChildren(ctx)793+ templ_7745c5c3_Var39 := templ.GetChildren(ctx)
684- if templ_7745c5c3_Var31 == nil {794+ if templ_7745c5c3_Var39 == nil {
685- templ_7745c5c3_Var31 = templ.NopComponent795+ templ_7745c5c3_Var39 = templ.NopComponent
686 }796 }
687 ctx = templ.ClearChildren(ctx)797 ctx = templ.ClearChildren(ctx)
688- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<tr")798+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<tr")
689 if templ_7745c5c3_Err != nil {799 if templ_7745c5c3_Err != nil {
690 return templ_7745c5c3_Err800 return templ_7745c5c3_Err
691 }801 }
692 if r.Cursor {802 if r.Cursor {
693- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " data-cursor")803+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " data-cursor")
694 if templ_7745c5c3_Err != nil {804 if templ_7745c5c3_Err != nil {
695 return templ_7745c5c3_Err805 return templ_7745c5c3_Err
696 }806 }
697 }807 }
698- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "><td class=\"c-kind mute\">")808+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "><td class=\"c-kind mute\">")
699 if templ_7745c5c3_Err != nil {809 if templ_7745c5c3_Err != nil {
700 return templ_7745c5c3_Err810 return templ_7745c5c3_Err
701 }811 }
702- var templ_7745c5c3_Var32 string812+ var templ_7745c5c3_Var40 string
703- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(kind)813+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(kind)
704 if templ_7745c5c3_Err != nil {814 if templ_7745c5c3_Err != nil {
705- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 161, Col: 32}815+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 169, Col: 32}
706 }816 }
707- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))817+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
708 if templ_7745c5c3_Err != nil {818 if templ_7745c5c3_Err != nil {
709 return templ_7745c5c3_Err819 return templ_7745c5c3_Err
710 }820 }
711- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</td><td class=\"c-name\"><a href=\"")821+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</td><td class=\"c-name\"><a href=\"")
712 if templ_7745c5c3_Err != nil {822 if templ_7745c5c3_Err != nil {
713 return templ_7745c5c3_Err823 return templ_7745c5c3_Err
714 }824 }
715- var templ_7745c5c3_Var33 templ.SafeURL825+ var templ_7745c5c3_Var41 templ.SafeURL
716- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))826+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
717 if templ_7745c5c3_Err != nil {827 if templ_7745c5c3_Err != nil {
718- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 163, Col: 34}828+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 171, Col: 34}
719 }829 }
720- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))830+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
721 if templ_7745c5c3_Err != nil {831 if templ_7745c5c3_Err != nil {
722 return templ_7745c5c3_Err832 return templ_7745c5c3_Err
723 }833 }
724- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">")834+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">")
725 if templ_7745c5c3_Err != nil {835 if templ_7745c5c3_Err != nil {
726 return templ_7745c5c3_Err836 return templ_7745c5c3_Err
727 }837 }
728- var templ_7745c5c3_Var34 string838+ var templ_7745c5c3_Var42 string
729- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)839+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
730 if templ_7745c5c3_Err != nil {840 if templ_7745c5c3_Err != nil {
731- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 163, Col: 45}841+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 171, Col: 45}
732 }842 }
733- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))843+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
734 if templ_7745c5c3_Err != nil {844 if templ_7745c5c3_Err != nil {
735 return templ_7745c5c3_Err845 return templ_7745c5c3_Err
736 }846 }
737- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</a> ")847+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</a> ")
738 if templ_7745c5c3_Err != nil {848 if templ_7745c5c3_Err != nil {
739 return templ_7745c5c3_Err849 return templ_7745c5c3_Err
740 }850 }
741 if r.Default {851 if r.Default {
742- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<span class=\"pill\">default</span>")852+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<span class=\"pill\">default</span>")
743 if templ_7745c5c3_Err != nil {853 if templ_7745c5c3_Err != nil {
744 return templ_7745c5c3_Err854 return templ_7745c5c3_Err
745 }855 }
746 }856 }
747- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</td><td class=\"c-sha\">")857+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</td><td class=\"c-sha\">")
748 if templ_7745c5c3_Err != nil {858 if templ_7745c5c3_Err != nil {
749 return templ_7745c5c3_Err859 return templ_7745c5c3_Err
750 }860 }
751 if r.CHref != "" {861 if r.CHref != "" {
752- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a href=\"")862+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a href=\"")
753 if templ_7745c5c3_Err != nil {863 if templ_7745c5c3_Err != nil {
754 return templ_7745c5c3_Err864 return templ_7745c5c3_Err
755 }865 }
756- var templ_7745c5c3_Var35 templ.SafeURL866+ var templ_7745c5c3_Var43 templ.SafeURL
757- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))867+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
758 if templ_7745c5c3_Err != nil {868 if templ_7745c5c3_Err != nil {
759- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 170, Col: 36}869+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 178, Col: 36}
760 }870 }
761- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))871+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
762 if templ_7745c5c3_Err != nil {872 if templ_7745c5c3_Err != nil {
763 return templ_7745c5c3_Err873 return templ_7745c5c3_Err
764 }874 }
765- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\">")875+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\">")
766 if templ_7745c5c3_Err != nil {876 if templ_7745c5c3_Err != nil {
767 return templ_7745c5c3_Err877 return templ_7745c5c3_Err
768 }878 }
769- var templ_7745c5c3_Var36 string879+ var templ_7745c5c3_Var44 string
770- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)880+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
771 if templ_7745c5c3_Err != nil {881 if templ_7745c5c3_Err != nil {
772- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 170, Col: 48}882+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 178, Col: 48}
773 }883 }
774- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))884+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
775 if templ_7745c5c3_Err != nil {885 if templ_7745c5c3_Err != nil {
776 return templ_7745c5c3_Err886 return templ_7745c5c3_Err
777 }887 }
778- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</a>")888+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</a>")
779 if templ_7745c5c3_Err != nil {889 if templ_7745c5c3_Err != nil {
780 return templ_7745c5c3_Err890 return templ_7745c5c3_Err
781 }891 }
782 } else {892 } else {
783- var templ_7745c5c3_Var37 string893+ var templ_7745c5c3_Var45 string
784- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)894+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
785 if templ_7745c5c3_Err != nil {895 if templ_7745c5c3_Err != nil {
786- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 172, Col: 13}896+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 180, Col: 13}
787 }897 }
788- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))898+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
789 if templ_7745c5c3_Err != nil {899 if templ_7745c5c3_Err != nil {
790 return templ_7745c5c3_Err900 return templ_7745c5c3_Err
791 }901 }
@@ -794,69 +904,69 @@ func refRow(kind string, r RefRow) templ.Component {
794 if templ_7745c5c3_Err != nil {904 if templ_7745c5c3_Err != nil {
795 return templ_7745c5c3_Err905 return templ_7745c5c3_Err
796 }906 }
797- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</td><td class=\"c-sub\">")907+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</td><td class=\"c-sub\">")
798 if templ_7745c5c3_Err != nil {908 if templ_7745c5c3_Err != nil {
799 return templ_7745c5c3_Err909 return templ_7745c5c3_Err
800 }910 }
801- var templ_7745c5c3_Var38 string911+ var templ_7745c5c3_Var46 string
802- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)912+ templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)
803 if templ_7745c5c3_Err != nil {913 if templ_7745c5c3_Err != nil {
804- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 176, Col: 31}914+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 184, Col: 31}
805 }915 }
806- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))916+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
807 if templ_7745c5c3_Err != nil {917 if templ_7745c5c3_Err != nil {
808 return templ_7745c5c3_Err918 return templ_7745c5c3_Err
809 }919 }
810- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</td><td class=\"c-when dim\">")920+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</td><td class=\"c-when dim\">")
811 if templ_7745c5c3_Err != nil {921 if templ_7745c5c3_Err != nil {
812 return templ_7745c5c3_Err922 return templ_7745c5c3_Err
813 }923 }
814 if r.WhenAt != "" {924 if r.WhenAt != "" {
815- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<time datetime=\"")925+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<time datetime=\"")
816 if templ_7745c5c3_Err != nil {926 if templ_7745c5c3_Err != nil {
817 return templ_7745c5c3_Err927 return templ_7745c5c3_Err
818 }928 }
819- var templ_7745c5c3_Var39 string929+ var templ_7745c5c3_Var47 string
820- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)930+ templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
821 if templ_7745c5c3_Err != nil {931 if templ_7745c5c3_Err != nil {
822- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 179, Col: 29}932+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 29}
823 }933 }
824- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)934+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47)
825 if templ_7745c5c3_Err != nil {935 if templ_7745c5c3_Err != nil {
826 return templ_7745c5c3_Err936 return templ_7745c5c3_Err
827 }937 }
828- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" title=\"")938+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" title=\"")
829 if templ_7745c5c3_Err != nil {939 if templ_7745c5c3_Err != nil {
830 return templ_7745c5c3_Err940 return templ_7745c5c3_Err
831 }941 }
832- var templ_7745c5c3_Var40 string942+ var templ_7745c5c3_Var48 string
833- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)943+ templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
834 if templ_7745c5c3_Err != nil {944 if templ_7745c5c3_Err != nil {
835- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 179, Col: 48}945+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 48}
836 }946 }
837- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40)947+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48)
838 if templ_7745c5c3_Err != nil {948 if templ_7745c5c3_Err != nil {
839 return templ_7745c5c3_Err949 return templ_7745c5c3_Err
840 }950 }
841- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\">")951+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\">")
842 if templ_7745c5c3_Err != nil {952 if templ_7745c5c3_Err != nil {
843 return templ_7745c5c3_Err953 return templ_7745c5c3_Err
844 }954 }
845- var templ_7745c5c3_Var41 string955+ var templ_7745c5c3_Var49 string
846- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)956+ templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
847 if templ_7745c5c3_Err != nil {957 if templ_7745c5c3_Err != nil {
848- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 179, Col: 59}958+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 59}
849 }959 }
850- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))960+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
851 if templ_7745c5c3_Err != nil {961 if templ_7745c5c3_Err != nil {
852 return templ_7745c5c3_Err962 return templ_7745c5c3_Err
853 }963 }
854- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</time>")964+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</time>")
855 if templ_7745c5c3_Err != nil {965 if templ_7745c5c3_Err != nil {
856 return templ_7745c5c3_Err966 return templ_7745c5c3_Err
857 }967 }
858 }968 }
859- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</td></tr>")969+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</td></tr>")
860 if templ_7745c5c3_Err != nil {970 if templ_7745c5c3_Err != nil {
861 return templ_7745c5c3_Err971 return templ_7745c5c3_Err
862 }972 }
@@ -880,12 +990,12 @@ func refsRail(p RefsPage) templ.Component {
880 }()990 }()
881 }991 }
882 ctx = templ.InitializeContext(ctx)992 ctx = templ.InitializeContext(ctx)
883- templ_7745c5c3_Var42 := templ.GetChildren(ctx)993+ templ_7745c5c3_Var50 := templ.GetChildren(ctx)
884- if templ_7745c5c3_Var42 == nil {994+ if templ_7745c5c3_Var50 == nil {
885- templ_7745c5c3_Var42 = templ.NopComponent995+ templ_7745c5c3_Var50 = templ.NopComponent
886 }996 }
887 ctx = templ.ClearChildren(ctx)997 ctx = templ.ClearChildren(ctx)
888- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>")998+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>")
889 if templ_7745c5c3_Err != nil {999 if templ_7745c5c3_Err != nil {
890 return templ_7745c5c3_Err1000 return templ_7745c5c3_Err
891 }1001 }
@@ -893,7 +1003,7 @@ func refsRail(p RefsPage) templ.Component {
893 if templ_7745c5c3_Err != nil {1003 if templ_7745c5c3_Err != nil {
894 return templ_7745c5c3_Err1004 return templ_7745c5c3_Err
895 }1005 }
896- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</section>")1006+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</section>")
897 if templ_7745c5c3_Err != nil {1007 if templ_7745c5c3_Err != nil {
898 return templ_7745c5c3_Err1008 return templ_7745c5c3_Err
899 }1009 }
@@ -918,12 +1028,12 @@ func State(l Layout, p StatePage) templ.Component {
918 }()1028 }()
919 }1029 }
920 ctx = templ.InitializeContext(ctx)1030 ctx = templ.InitializeContext(ctx)
921- templ_7745c5c3_Var43 := templ.GetChildren(ctx)1031+ templ_7745c5c3_Var51 := templ.GetChildren(ctx)
922- if templ_7745c5c3_Var43 == nil {1032+ if templ_7745c5c3_Var51 == nil {
923- templ_7745c5c3_Var43 = templ.NopComponent1033+ templ_7745c5c3_Var51 = templ.NopComponent
924 }1034 }
925 ctx = templ.ClearChildren(ctx)1035 ctx = templ.ClearChildren(ctx)
926- templ_7745c5c3_Var44 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {1036+ templ_7745c5c3_Var52 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
927 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context1037 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
928 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)1038 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
929 if !templ_7745c5c3_IsBuffer {1039 if !templ_7745c5c3_IsBuffer {
@@ -935,51 +1045,51 @@ func State(l Layout, p StatePage) templ.Component {
935 }()1045 }()
936 }1046 }
937 ctx = templ.InitializeContext(ctx)1047 ctx = templ.InitializeContext(ctx)
938- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">")1048+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">")
939 if templ_7745c5c3_Err != nil {1049 if templ_7745c5c3_Err != nil {
940 return templ_7745c5c3_Err1050 return templ_7745c5c3_Err
941 }1051 }
942- var templ_7745c5c3_Var45 string1052+ var templ_7745c5c3_Var53 string
943- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code)1053+ templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code)
944 if templ_7745c5c3_Err != nil {1054 if templ_7745c5c3_Err != nil {
945- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 196, Col: 30}1055+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 204, Col: 30}
946 }1056 }
947- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))1057+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
948 if templ_7745c5c3_Err != nil {1058 if templ_7745c5c3_Err != nil {
949 return templ_7745c5c3_Err1059 return templ_7745c5c3_Err
950 }1060 }
951- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</p><h1 id=\"state-title\">")1061+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</p><h1 id=\"state-title\">")
952 if templ_7745c5c3_Err != nil {1062 if templ_7745c5c3_Err != nil {
953 return templ_7745c5c3_Err1063 return templ_7745c5c3_Err
954 }1064 }
955- var templ_7745c5c3_Var46 string1065+ var templ_7745c5c3_Var54 string
956- templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)1066+ templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
957 if templ_7745c5c3_Err != nil {1067 if templ_7745c5c3_Err != nil {
958- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 197, Col: 33}1068+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 205, Col: 33}
959 }1069 }
960- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))1070+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
961 if templ_7745c5c3_Err != nil {1071 if templ_7745c5c3_Err != nil {
962 return templ_7745c5c3_Err1072 return templ_7745c5c3_Err
963 }1073 }
964- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</h1>")1074+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</h1>")
965 if templ_7745c5c3_Err != nil {1075 if templ_7745c5c3_Err != nil {
966 return templ_7745c5c3_Err1076 return templ_7745c5c3_Err
967 }1077 }
968 for _, line := range p.Lines {1078 for _, line := range p.Lines {
969- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<p class=\"dim\">")1079+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<p class=\"dim\">")
970 if templ_7745c5c3_Err != nil {1080 if templ_7745c5c3_Err != nil {
971 return templ_7745c5c3_Err1081 return templ_7745c5c3_Err
972 }1082 }
973- var templ_7745c5c3_Var47 string1083+ var templ_7745c5c3_Var55 string
974- templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(line)1084+ templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(line)
975 if templ_7745c5c3_Err != nil {1085 if templ_7745c5c3_Err != nil {
976- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 199, Col: 25}1086+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 207, Col: 25}
977 }1087 }
978- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))1088+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
979 if templ_7745c5c3_Err != nil {1089 if templ_7745c5c3_Err != nil {
980 return templ_7745c5c3_Err1090 return templ_7745c5c3_Err
981 }1091 }
982- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</p>")1092+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</p>")
983 if templ_7745c5c3_Err != nil {1093 if templ_7745c5c3_Err != nil {
984 return templ_7745c5c3_Err1094 return templ_7745c5c3_Err
985 }1095 }
@@ -990,49 +1100,49 @@ func State(l Layout, p StatePage) templ.Component {
990 return templ_7745c5c3_Err1100 return templ_7745c5c3_Err
991 }1101 }
992 }1102 }
993- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<p class=\"state-back\">")1103+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<p class=\"state-back\">")
994 if templ_7745c5c3_Err != nil {1104 if templ_7745c5c3_Err != nil {
995 return templ_7745c5c3_Err1105 return templ_7745c5c3_Err
996 }1106 }
997 for _, b := range p.Back {1107 for _, b := range p.Back {
998- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<a href=\"")1108+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<a href=\"")
999 if templ_7745c5c3_Err != nil {1109 if templ_7745c5c3_Err != nil {
1000 return templ_7745c5c3_Err1110 return templ_7745c5c3_Err
1001 }1111 }
1002- var templ_7745c5c3_Var48 templ.SafeURL1112+ var templ_7745c5c3_Var56 templ.SafeURL
1003- templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href))1113+ templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href))
1004 if templ_7745c5c3_Err != nil {1114 if templ_7745c5c3_Err != nil {
1005- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 206, Col: 36}1115+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 214, Col: 36}
1006 }1116 }
1007- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))1117+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
1008 if templ_7745c5c3_Err != nil {1118 if templ_7745c5c3_Err != nil {
1009 return templ_7745c5c3_Err1119 return templ_7745c5c3_Err
1010 }1120 }
1011- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\">")1121+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "\">")
1012 if templ_7745c5c3_Err != nil {1122 if templ_7745c5c3_Err != nil {
1013 return templ_7745c5c3_Err1123 return templ_7745c5c3_Err
1014 }1124 }
1015- var templ_7745c5c3_Var49 string1125+ var templ_7745c5c3_Var57 string
1016- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label)1126+ templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label)
1017 if templ_7745c5c3_Err != nil {1127 if templ_7745c5c3_Err != nil {
1018- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 206, Col: 48}1128+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 214, Col: 48}
1019 }1129 }
1020- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))1130+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
1021 if templ_7745c5c3_Err != nil {1131 if templ_7745c5c3_Err != nil {
1022 return templ_7745c5c3_Err1132 return templ_7745c5c3_Err
1023 }1133 }
1024- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</a>")1134+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</a>")
1025 if templ_7745c5c3_Err != nil {1135 if templ_7745c5c3_Err != nil {
1026 return templ_7745c5c3_Err1136 return templ_7745c5c3_Err
1027 }1137 }
1028 }1138 }
1029- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</p></section>")1139+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</p></section>")
1030 if templ_7745c5c3_Err != nil {1140 if templ_7745c5c3_Err != nil {
1031 return templ_7745c5c3_Err1141 return templ_7745c5c3_Err
1032 }1142 }
1033 return nil1143 return nil
1034 })1144 })
1035- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var44), templ_7745c5c3_Buffer)1145+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var52), templ_7745c5c3_Buffer)
1036 if templ_7745c5c3_Err != nil {1146 if templ_7745c5c3_Err != nil {
1037 return templ_7745c5c3_Err1147 return templ_7745c5c3_Err
1038 }1148 }
@@ -1061,12 +1171,12 @@ func Login(l Layout, p LoginPage) templ.Component {
1061 }()1171 }()
1062 }1172 }
1063 ctx = templ.InitializeContext(ctx)1173 ctx = templ.InitializeContext(ctx)
1064- templ_7745c5c3_Var50 := templ.GetChildren(ctx)1174+ templ_7745c5c3_Var58 := templ.GetChildren(ctx)
1065- if templ_7745c5c3_Var50 == nil {1175+ if templ_7745c5c3_Var58 == nil {
1066- templ_7745c5c3_Var50 = templ.NopComponent1176+ templ_7745c5c3_Var58 = templ.NopComponent
1067 }1177 }
1068 ctx = templ.ClearChildren(ctx)1178 ctx = templ.ClearChildren(ctx)
1069- templ_7745c5c3_Var51 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {1179+ templ_7745c5c3_Var59 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1070 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context1180 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
1071 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)1181 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
1072 if !templ_7745c5c3_IsBuffer {1182 if !templ_7745c5c3_IsBuffer {
@@ -1078,75 +1188,75 @@ func Login(l Layout, p LoginPage) templ.Component {
1078 }()1188 }()
1079 }1189 }
1080 ctx = templ.InitializeContext(ctx)1190 ctx = templ.InitializeContext(ctx)
1081- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")1191+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
1082 if templ_7745c5c3_Err != nil {1192 if templ_7745c5c3_Err != nil {
1083 return templ_7745c5c3_Err1193 return templ_7745c5c3_Err
1084 }1194 }
1085- var templ_7745c5c3_Var52 string1195+ var templ_7745c5c3_Var60 string
1086- templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)1196+ templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
1087 if templ_7745c5c3_Err != nil {1197 if templ_7745c5c3_Err != nil {
1088- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 221, Col: 33}1198+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 229, Col: 33}
1089 }1199 }
1090- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))1200+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60))
1091 if templ_7745c5c3_Err != nil {1201 if templ_7745c5c3_Err != nil {
1092 return templ_7745c5c3_Err1202 return templ_7745c5c3_Err
1093 }1203 }
1094- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</h1>")1204+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</h1>")
1095 if templ_7745c5c3_Err != nil {1205 if templ_7745c5c3_Err != nil {
1096 return templ_7745c5c3_Err1206 return templ_7745c5c3_Err
1097 }1207 }
1098 for _, line := range p.Lines {1208 for _, line := range p.Lines {
1099- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<p class=\"dim\">")1209+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<p class=\"dim\">")
1100 if templ_7745c5c3_Err != nil {1210 if templ_7745c5c3_Err != nil {
1101 return templ_7745c5c3_Err1211 return templ_7745c5c3_Err
1102 }1212 }
1103- var templ_7745c5c3_Var53 string1213+ var templ_7745c5c3_Var61 string
1104- templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(line)1214+ templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(line)
1105 if templ_7745c5c3_Err != nil {1215 if templ_7745c5c3_Err != nil {
1106- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 223, Col: 25}1216+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 231, Col: 25}
1107 }1217 }
1108- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))1218+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
1109 if templ_7745c5c3_Err != nil {1219 if templ_7745c5c3_Err != nil {
1110 return templ_7745c5c3_Err1220 return templ_7745c5c3_Err
1111 }1221 }
1112- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</p>")1222+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</p>")
1113 if templ_7745c5c3_Err != nil {1223 if templ_7745c5c3_Err != nil {
1114 return templ_7745c5c3_Err1224 return templ_7745c5c3_Err
1115 }1225 }
1116 }1226 }
1117- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<form class=\"state-act\" method=\"post\" action=\"")1227+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "<form class=\"state-act\" method=\"post\" action=\"")
1118 if templ_7745c5c3_Err != nil {1228 if templ_7745c5c3_Err != nil {
1119 return templ_7745c5c3_Err1229 return templ_7745c5c3_Err
1120 }1230 }
1121- var templ_7745c5c3_Var54 templ.SafeURL1231+ var templ_7745c5c3_Var62 templ.SafeURL
1122- templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))1232+ templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))
1123 if templ_7745c5c3_Err != nil {1233 if templ_7745c5c3_Err != nil {
1124- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 225, Col: 73}1234+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 233, Col: 73}
1125 }1235 }
1126- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))1236+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
1127 if templ_7745c5c3_Err != nil {1237 if templ_7745c5c3_Err != nil {
1128 return templ_7745c5c3_Err1238 return templ_7745c5c3_Err
1129 }1239 }
1130- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">")1240+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">")
1131 if templ_7745c5c3_Err != nil {1241 if templ_7745c5c3_Err != nil {
1132 return templ_7745c5c3_Err1242 return templ_7745c5c3_Err
1133 }1243 }
1134- var templ_7745c5c3_Var55 string1244+ var templ_7745c5c3_Var63 string
1135- templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)1245+ templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
1136 if templ_7745c5c3_Err != nil {1246 if templ_7745c5c3_Err != nil {
1137- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 226, Col: 56}1247+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 234, Col: 56}
1138 }1248 }
1139- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))1249+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
1140 if templ_7745c5c3_Err != nil {1250 if templ_7745c5c3_Err != nil {
1141 return templ_7745c5c3_Err1251 return templ_7745c5c3_Err
1142 }1252 }
1143- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")1253+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
1144 if templ_7745c5c3_Err != nil {1254 if templ_7745c5c3_Err != nil {
1145 return templ_7745c5c3_Err1255 return templ_7745c5c3_Err
1146 }1256 }
1147 return nil1257 return nil
1148 })1258 })
1149- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var51), templ_7745c5c3_Buffer)1259+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var59), templ_7745c5c3_Buffer)
1150 if templ_7745c5c3_Err != nil {1260 if templ_7745c5c3_Err != nil {
1151 return templ_7745c5c3_Err1261 return templ_7745c5c3_Err
1152 }1262 }
@@ -1172,12 +1282,12 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component {
1172 }()1282 }()
1173 }1283 }
1174 ctx = templ.InitializeContext(ctx)1284 ctx = templ.InitializeContext(ctx)
1175- templ_7745c5c3_Var56 := templ.GetChildren(ctx)1285+ templ_7745c5c3_Var64 := templ.GetChildren(ctx)
1176- if templ_7745c5c3_Var56 == nil {1286+ if templ_7745c5c3_Var64 == nil {
1177- templ_7745c5c3_Var56 = templ.NopComponent1287+ templ_7745c5c3_Var64 = templ.NopComponent
1178 }1288 }
1179 ctx = templ.ClearChildren(ctx)1289 ctx = templ.ClearChildren(ctx)
1180- templ_7745c5c3_Var57 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {1290+ templ_7745c5c3_Var65 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1181 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context1291 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
1182 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)1292 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
1183 if !templ_7745c5c3_IsBuffer {1293 if !templ_7745c5c3_IsBuffer {
@@ -1189,88 +1299,88 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component {
1189 }()1299 }()
1190 }1300 }
1191 ctx = templ.InitializeContext(ctx)1301 ctx = templ.InitializeContext(ctx)
1192- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")1302+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
1193 if templ_7745c5c3_Err != nil {1303 if templ_7745c5c3_Err != nil {
1194 return templ_7745c5c3_Err1304 return templ_7745c5c3_Err
1195 }1305 }
1196- var templ_7745c5c3_Var58 string1306+ var templ_7745c5c3_Var66 string
1197- templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)1307+ templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
1198 if templ_7745c5c3_Err != nil {1308 if templ_7745c5c3_Err != nil {
1199- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 238, Col: 33}1309+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 246, Col: 33}
1200 }1310 }
1201- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))1311+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))
1202 if templ_7745c5c3_Err != nil {1312 if templ_7745c5c3_Err != nil {
1203 return templ_7745c5c3_Err1313 return templ_7745c5c3_Err
1204 }1314 }
1205- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</h1><form class=\"state-act\" data-js data-passkey-form><button type=\"submit\" class=\"btn primary\" data-passkey=\"login\" data-begin=\"")1315+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "</h1><form class=\"state-act\" data-js data-passkey-form><button type=\"submit\" class=\"btn primary\" data-passkey=\"login\" data-begin=\"")
1206 if templ_7745c5c3_Err != nil {1316 if templ_7745c5c3_Err != nil {
1207 return templ_7745c5c3_Err1317 return templ_7745c5c3_Err
1208 }1318 }
1209- var templ_7745c5c3_Var59 string1319+ var templ_7745c5c3_Var67 string
1210- templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)1320+ templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
1211 if templ_7745c5c3_Err != nil {1321 if templ_7745c5c3_Err != nil {
1212- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 240, Col: 87}1322+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 87}
1213 }1323 }
1214- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59)1324+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
1215 if templ_7745c5c3_Err != nil {1325 if templ_7745c5c3_Err != nil {
1216 return templ_7745c5c3_Err1326 return templ_7745c5c3_Err
1217 }1327 }
1218- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\" data-finish=\"")1328+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "\" data-finish=\"")
1219 if templ_7745c5c3_Err != nil {1329 if templ_7745c5c3_Err != nil {
1220 return templ_7745c5c3_Err1330 return templ_7745c5c3_Err
1221 }1331 }
1222- var templ_7745c5c3_Var60 string1332+ var templ_7745c5c3_Var68 string
1223- templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)1333+ templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
1224 if templ_7745c5c3_Err != nil {1334 if templ_7745c5c3_Err != nil {
1225- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 240, Col: 112}1335+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 112}
1226 }1336 }
1227- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)1337+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var68)
1228 if templ_7745c5c3_Err != nil {1338 if templ_7745c5c3_Err != nil {
1229 return templ_7745c5c3_Err1339 return templ_7745c5c3_Err
1230 }1340 }
1231- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "\">")1341+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "\">")
1232 if templ_7745c5c3_Err != nil {1342 if templ_7745c5c3_Err != nil {
1233 return templ_7745c5c3_Err1343 return templ_7745c5c3_Err
1234 }1344 }
1235- var templ_7745c5c3_Var61 string1345+ var templ_7745c5c3_Var69 string
1236- templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)1346+ templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
1237 if templ_7745c5c3_Err != nil {1347 if templ_7745c5c3_Err != nil {
1238- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 240, Col: 125}1348+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 125}
1239 }1349 }
1240- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))1350+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
1241 if templ_7745c5c3_Err != nil {1351 if templ_7745c5c3_Err != nil {
1242 return templ_7745c5c3_Err1352 return templ_7745c5c3_Err
1243 }1353 }
1244- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")1354+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
1245 if templ_7745c5c3_Err != nil {1355 if templ_7745c5c3_Err != nil {
1246 return templ_7745c5c3_Err1356 return templ_7745c5c3_Err
1247 }1357 }
1248 for _, line := range p.Lines {1358 for _, line := range p.Lines {
1249- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<p class=\"dim\">")1359+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<p class=\"dim\">")
1250 if templ_7745c5c3_Err != nil {1360 if templ_7745c5c3_Err != nil {
1251 return templ_7745c5c3_Err1361 return templ_7745c5c3_Err
1252 }1362 }
1253- var templ_7745c5c3_Var62 string1363+ var templ_7745c5c3_Var70 string
1254- templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(line)1364+ templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(line)
1255 if templ_7745c5c3_Err != nil {1365 if templ_7745c5c3_Err != nil {
1256- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 244, Col: 25}1366+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 252, Col: 25}
1257 }1367 }
1258- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))1368+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
1259 if templ_7745c5c3_Err != nil {1369 if templ_7745c5c3_Err != nil {
1260 return templ_7745c5c3_Err1370 return templ_7745c5c3_Err
1261 }1371 }
1262- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</p>")1372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "</p>")
1263 if templ_7745c5c3_Err != nil {1373 if templ_7745c5c3_Err != nil {
1264 return templ_7745c5c3_Err1374 return templ_7745c5c3_Err
1265 }1375 }
1266 }1376 }
1267- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")1377+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
1268 if templ_7745c5c3_Err != nil {1378 if templ_7745c5c3_Err != nil {
1269 return templ_7745c5c3_Err1379 return templ_7745c5c3_Err
1270 }1380 }
1271 return nil1381 return nil
1272 })1382 })
1273- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var57), templ_7745c5c3_Buffer)1383+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var65), templ_7745c5c3_Buffer)
1274 if templ_7745c5c3_Err != nil {1384 if templ_7745c5c3_Err != nil {
1275 return templ_7745c5c3_Err1385 return templ_7745c5c3_Err
1276 }1386 }
@@ -1295,12 +1405,12 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component {
1295 }()1405 }()
1296 }1406 }
1297 ctx = templ.InitializeContext(ctx)1407 ctx = templ.InitializeContext(ctx)
1298- templ_7745c5c3_Var63 := templ.GetChildren(ctx)1408+ templ_7745c5c3_Var71 := templ.GetChildren(ctx)
1299- if templ_7745c5c3_Var63 == nil {1409+ if templ_7745c5c3_Var71 == nil {
1300- templ_7745c5c3_Var63 = templ.NopComponent1410+ templ_7745c5c3_Var71 = templ.NopComponent
1301 }1411 }
1302 ctx = templ.ClearChildren(ctx)1412 ctx = templ.ClearChildren(ctx)
1303- templ_7745c5c3_Var64 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {1413+ templ_7745c5c3_Var72 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1304 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context1414 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
1305 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)1415 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
1306 if !templ_7745c5c3_IsBuffer {1416 if !templ_7745c5c3_IsBuffer {
@@ -1312,231 +1422,231 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component {
1312 }()1422 }()
1313 }1423 }
1314 ctx = templ.InitializeContext(ctx)1424 ctx = templ.InitializeContext(ctx)
1315- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">")1425+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">")
1316 if templ_7745c5c3_Err != nil {1426 if templ_7745c5c3_Err != nil {
1317 return templ_7745c5c3_Err1427 return templ_7745c5c3_Err
1318 }1428 }
1319- var templ_7745c5c3_Var65 string1429+ var templ_7745c5c3_Var73 string
1320- templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))1430+ templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))
1321 if templ_7745c5c3_Err != nil {1431 if templ_7745c5c3_Err != nil {
1322- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 254, Col: 81}1432+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 262, Col: 81}
1323 }1433 }
1324- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))1434+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
1325 if templ_7745c5c3_Err != nil {1435 if templ_7745c5c3_Err != nil {
1326 return templ_7745c5c3_Err1436 return templ_7745c5c3_Err
1327 }1437 }
1328- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "</span></h2><p class=\"dim\">A passkey signs you in from this device or your phone, without a terminal. Removing one signs every browser out.</p>")1438+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</span></h2><p class=\"dim\">A passkey signs you in from this device or your phone, without a terminal. Removing one signs every browser out.</p>")
1329 if templ_7745c5c3_Err != nil {1439 if templ_7745c5c3_Err != nil {
1330 return templ_7745c5c3_Err1440 return templ_7745c5c3_Err
1331 }1441 }
1332 if len(p.Rows) > 0 {1442 if len(p.Rows) > 0 {
1333- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "<table class=\"list passkeys\" aria-label=\"Passkeys\"><thead><tr><th class=\"c-name\">Name</th><th class=\"c-when\">Created</th><th class=\"c-when\">Last used</th><th class=\"c-sync\">Synced</th><th class=\"c-act\"><span class=\"sr\">Remove</span></th></tr></thead> <tbody>")1443+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<table class=\"list passkeys\" aria-label=\"Passkeys\"><thead><tr><th class=\"c-name\">Name</th><th class=\"c-when\">Created</th><th class=\"c-when\">Last used</th><th class=\"c-sync\">Synced</th><th class=\"c-act\"><span class=\"sr\">Remove</span></th></tr></thead> <tbody>")
1334 if templ_7745c5c3_Err != nil {1444 if templ_7745c5c3_Err != nil {
1335 return templ_7745c5c3_Err1445 return templ_7745c5c3_Err
1336 }1446 }
1337 for _, r := range p.Rows {1447 for _, r := range p.Rows {
1338- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "<tr><td class=\"c-name\">")1448+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "<tr><td class=\"c-name\">")
1339 if templ_7745c5c3_Err != nil {1449 if templ_7745c5c3_Err != nil {
1340 return templ_7745c5c3_Err1450 return templ_7745c5c3_Err
1341 }1451 }
1342- var templ_7745c5c3_Var66 string1452+ var templ_7745c5c3_Var74 string
1343- templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)1453+ templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
1344 if templ_7745c5c3_Err != nil {1454 if templ_7745c5c3_Err != nil {
1345- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 270, Col: 34}1455+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 278, Col: 34}
1346 }1456 }
1347- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))1457+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74))
1348 if templ_7745c5c3_Err != nil {1458 if templ_7745c5c3_Err != nil {
1349 return templ_7745c5c3_Err1459 return templ_7745c5c3_Err
1350 }1460 }
1351- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</td><td class=\"c-when nowrap\"><time")1461+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</td><td class=\"c-when nowrap\"><time")
1352 if templ_7745c5c3_Err != nil {1462 if templ_7745c5c3_Err != nil {
1353 return templ_7745c5c3_Err1463 return templ_7745c5c3_Err
1354 }1464 }
1355 if r.CreatedAt != "" {1465 if r.CreatedAt != "" {
1356- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, " datetime=\"")1466+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, " datetime=\"")
1357 if templ_7745c5c3_Err != nil {1467 if templ_7745c5c3_Err != nil {
1358 return templ_7745c5c3_Err1468 return templ_7745c5c3_Err
1359 }1469 }
1360- var templ_7745c5c3_Var67 string1470+ var templ_7745c5c3_Var75 string
1361- templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt)1471+ templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt)
1362 if templ_7745c5c3_Err != nil {1472 if templ_7745c5c3_Err != nil {
1363- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 274, Col: 32}1473+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 282, Col: 32}
1364 }1474 }
1365- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)1475+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75)
1366 if templ_7745c5c3_Err != nil {1476 if templ_7745c5c3_Err != nil {
1367 return templ_7745c5c3_Err1477 return templ_7745c5c3_Err
1368 }1478 }
1369- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "\"")1479+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"")
1370 if templ_7745c5c3_Err != nil {1480 if templ_7745c5c3_Err != nil {
1371 return templ_7745c5c3_Err1481 return templ_7745c5c3_Err
1372 }1482 }
1373 }1483 }
1374- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, ">")1484+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, ">")
1375 if templ_7745c5c3_Err != nil {1485 if templ_7745c5c3_Err != nil {
1376 return templ_7745c5c3_Err1486 return templ_7745c5c3_Err
1377 }1487 }
1378- var templ_7745c5c3_Var68 string1488+ var templ_7745c5c3_Var76 string
1379- templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created)1489+ templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created)
1380 if templ_7745c5c3_Err != nil {1490 if templ_7745c5c3_Err != nil {
1381- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 276, Col: 20}1491+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 284, Col: 20}
1382 }1492 }
1383- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))1493+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
1384 if templ_7745c5c3_Err != nil {1494 if templ_7745c5c3_Err != nil {
1385 return templ_7745c5c3_Err1495 return templ_7745c5c3_Err
1386 }1496 }
1387- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "</time></td><td class=\"c-when nowrap\"><time")1497+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "</time></td><td class=\"c-when nowrap\"><time")
1388 if templ_7745c5c3_Err != nil {1498 if templ_7745c5c3_Err != nil {
1389 return templ_7745c5c3_Err1499 return templ_7745c5c3_Err
1390 }1500 }
1391 if r.LastUsedAt != "" {1501 if r.LastUsedAt != "" {
1392- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " datetime=\"")1502+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, " datetime=\"")
1393 if templ_7745c5c3_Err != nil {1503 if templ_7745c5c3_Err != nil {
1394 return templ_7745c5c3_Err1504 return templ_7745c5c3_Err
1395 }1505 }
1396- var templ_7745c5c3_Var69 string1506+ var templ_7745c5c3_Var77 string
1397- templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt)1507+ templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt)
1398 if templ_7745c5c3_Err != nil {1508 if templ_7745c5c3_Err != nil {
1399- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 281, Col: 33}1509+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 289, Col: 33}
1400 }1510 }
1401- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69)1511+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77)
1402 if templ_7745c5c3_Err != nil {1512 if templ_7745c5c3_Err != nil {
1403 return templ_7745c5c3_Err1513 return templ_7745c5c3_Err
1404 }1514 }
1405- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "\"")1515+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\"")
1406 if templ_7745c5c3_Err != nil {1516 if templ_7745c5c3_Err != nil {
1407 return templ_7745c5c3_Err1517 return templ_7745c5c3_Err
1408 }1518 }
1409 }1519 }
1410- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, ">")1520+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, ">")
1411 if templ_7745c5c3_Err != nil {1521 if templ_7745c5c3_Err != nil {
1412 return templ_7745c5c3_Err1522 return templ_7745c5c3_Err
1413 }1523 }
1414- var templ_7745c5c3_Var70 string1524+ var templ_7745c5c3_Var78 string
1415- templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed)1525+ templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed)
1416 if templ_7745c5c3_Err != nil {1526 if templ_7745c5c3_Err != nil {
1417- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 283, Col: 21}1527+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 291, Col: 21}
1418 }1528 }
1419- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))1529+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78))
1420 if templ_7745c5c3_Err != nil {1530 if templ_7745c5c3_Err != nil {
1421 return templ_7745c5c3_Err1531 return templ_7745c5c3_Err
1422 }1532 }
1423- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</time></td><td class=\"c-sync\">")1533+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</time></td><td class=\"c-sync\">")
1424 if templ_7745c5c3_Err != nil {1534 if templ_7745c5c3_Err != nil {
1425 return templ_7745c5c3_Err1535 return templ_7745c5c3_Err
1426 }1536 }
1427 if r.Synced {1537 if r.Synced {
1428- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "yes")1538+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "yes")
1429 if templ_7745c5c3_Err != nil {1539 if templ_7745c5c3_Err != nil {
1430 return templ_7745c5c3_Err1540 return templ_7745c5c3_Err
1431 }1541 }
1432 } else {1542 } else {
1433- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "no")1543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "no")
1434 if templ_7745c5c3_Err != nil {1544 if templ_7745c5c3_Err != nil {
1435 return templ_7745c5c3_Err1545 return templ_7745c5c3_Err
1436 }1546 }
1437 }1547 }
1438- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</td><td class=\"c-act\"><form method=\"post\" action=\"")1548+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</td><td class=\"c-act\"><form method=\"post\" action=\"")
1439 if templ_7745c5c3_Err != nil {1549 if templ_7745c5c3_Err != nil {
1440 return templ_7745c5c3_Err1550 return templ_7745c5c3_Err
1441 }1551 }
1442- var templ_7745c5c3_Var71 templ.SafeURL1552+ var templ_7745c5c3_Var79 templ.SafeURL
1443- templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove))1553+ templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove))
1444 if templ_7745c5c3_Err != nil {1554 if templ_7745c5c3_Err != nil {
1445- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 293, Col: 60}1555+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 301, Col: 60}
1446 }1556 }
1447- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))1557+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79))
1448 if templ_7745c5c3_Err != nil {1558 if templ_7745c5c3_Err != nil {
1449 return templ_7745c5c3_Err1559 return templ_7745c5c3_Err
1450 }1560 }
1451- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"")1561+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"")
1452 if templ_7745c5c3_Err != nil {1562 if templ_7745c5c3_Err != nil {
1453 return templ_7745c5c3_Err1563 return templ_7745c5c3_Err
1454 }1564 }
1455- var templ_7745c5c3_Var72 string1565+ var templ_7745c5c3_Var80 string
1456- templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name)1566+ templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name)
1457 if templ_7745c5c3_Err != nil {1567 if templ_7745c5c3_Err != nil {
1458- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 294, Col: 89}1568+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 302, Col: 89}
1459 }1569 }
1460- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)1570+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)
1461 if templ_7745c5c3_Err != nil {1571 if templ_7745c5c3_Err != nil {
1462 return templ_7745c5c3_Err1572 return templ_7745c5c3_Err
1463 }1573 }
1464- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "\">Remove</button></form></td></tr>")1574+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "\">Remove</button></form></td></tr>")
1465 if templ_7745c5c3_Err != nil {1575 if templ_7745c5c3_Err != nil {
1466 return templ_7745c5c3_Err1576 return templ_7745c5c3_Err
1467 }1577 }
1468 }1578 }
1469- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</tbody></table>")1579+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</tbody></table>")
1470 if templ_7745c5c3_Err != nil {1580 if templ_7745c5c3_Err != nil {
1471 return templ_7745c5c3_Err1581 return templ_7745c5c3_Err
1472 }1582 }
1473 } else {1583 } else {
1474- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<p class=\"lede dim\">no passkeys yet.</p>")1584+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<p class=\"lede dim\">no passkeys yet.</p>")
1475 if templ_7745c5c3_Err != nil {1585 if templ_7745c5c3_Err != nil {
1476 return templ_7745c5c3_Err1586 return templ_7745c5c3_Err
1477 }1587 }
1478 }1588 }
1479- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, " ")1589+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, " ")
1480 if templ_7745c5c3_Err != nil {1590 if templ_7745c5c3_Err != nil {
1481 return templ_7745c5c3_Err1591 return templ_7745c5c3_Err
1482 }1592 }
1483 if p.Full {1593 if p.Full {
1484- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "<p class=\"dim\">")1594+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<p class=\"dim\">")
1485 if templ_7745c5c3_Err != nil {1595 if templ_7745c5c3_Err != nil {
1486 return templ_7745c5c3_Err1596 return templ_7745c5c3_Err
1487 }1597 }
1488- var templ_7745c5c3_Var73 string1598+ var templ_7745c5c3_Var81 string
1489- templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max))1599+ templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max))
1490 if templ_7745c5c3_Err != nil {1600 if templ_7745c5c3_Err != nil {
1491- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 305, Col: 31}1601+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 313, Col: 31}
1492 }1602 }
1493- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))1603+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81))
1494 if templ_7745c5c3_Err != nil {1604 if templ_7745c5c3_Err != nil {
1495 return templ_7745c5c3_Err1605 return templ_7745c5c3_Err
1496 }1606 }
1497- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, " passkeys are registered, the most there can be. Remove one to add another.</p>")1607+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, " passkeys are registered, the most there can be. Remove one to add another.</p>")
1498 if templ_7745c5c3_Err != nil {1608 if templ_7745c5c3_Err != nil {
1499 return templ_7745c5c3_Err1609 return templ_7745c5c3_Err
1500 }1610 }
1501 } else {1611 } else {
1502- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<form class=\"passkey-add\" data-js data-passkey-form><label for=\"passkey-name\" class=\"sr\">Name of the new passkey</label> <input id=\"passkey-name\" type=\"text\" placeholder=\"Name, for example Pixel 9\" autocomplete=\"off\" data-passkey-name> <button type=\"submit\" class=\"btn primary\" data-passkey=\"register\" data-begin=\"")1612+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<form class=\"passkey-add\" data-js data-passkey-form><label for=\"passkey-name\" class=\"sr\">Name of the new passkey</label> <input id=\"passkey-name\" type=\"text\" placeholder=\"Name, for example Pixel 9\" autocomplete=\"off\" data-passkey-name> <button type=\"submit\" class=\"btn primary\" data-passkey=\"register\" data-begin=\"")
1503 if templ_7745c5c3_Err != nil {1613 if templ_7745c5c3_Err != nil {
1504 return templ_7745c5c3_Err1614 return templ_7745c5c3_Err
1505 }1615 }
1506- var templ_7745c5c3_Var74 string1616+ var templ_7745c5c3_Var82 string
1507- templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)1617+ templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
1508 if templ_7745c5c3_Err != nil {1618 if templ_7745c5c3_Err != nil {
1509- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 310, Col: 90}1619+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 318, Col: 90}
1510 }1620 }
1511- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74)1621+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82)
1512 if templ_7745c5c3_Err != nil {1622 if templ_7745c5c3_Err != nil {
1513 return templ_7745c5c3_Err1623 return templ_7745c5c3_Err
1514 }1624 }
1515- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "\" data-finish=\"")1625+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "\" data-finish=\"")
1516 if templ_7745c5c3_Err != nil {1626 if templ_7745c5c3_Err != nil {
1517 return templ_7745c5c3_Err1627 return templ_7745c5c3_Err
1518 }1628 }
1519- var templ_7745c5c3_Var75 string1629+ var templ_7745c5c3_Var83 string
1520- templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)1630+ templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
1521 if templ_7745c5c3_Err != nil {1631 if templ_7745c5c3_Err != nil {
1522- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 310, Col: 115}1632+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 318, Col: 115}
1523 }1633 }
1524- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75)1634+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83)
1525 if templ_7745c5c3_Err != nil {1635 if templ_7745c5c3_Err != nil {
1526 return templ_7745c5c3_Err1636 return templ_7745c5c3_Err
1527 }1637 }
1528- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\">Add a passkey</button></form>")1638+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "\">Add a passkey</button></form>")
1529 if templ_7745c5c3_Err != nil {1639 if templ_7745c5c3_Err != nil {
1530 return templ_7745c5c3_Err1640 return templ_7745c5c3_Err
1531 }1641 }
1532 }1642 }
1533- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")1643+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
1534 if templ_7745c5c3_Err != nil {1644 if templ_7745c5c3_Err != nil {
1535 return templ_7745c5c3_Err1645 return templ_7745c5c3_Err
1536 }1646 }
1537 return nil1647 return nil
1538 })1648 })
1539- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var64), templ_7745c5c3_Buffer)1649+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var72), templ_7745c5c3_Buffer)
1540 if templ_7745c5c3_Err != nil {1650 if templ_7745c5c3_Err != nil {
1541 return templ_7745c5c3_Err1651 return templ_7745c5c3_Err
1542 }1652 }
M internal/adapters/web/releases.templ +16 -9
@@ -40,15 +40,22 @@ templ Releases(l Layout, p ReleaseListPage) {
4040
4141 templ ReleaseDetail(l Layout, p ReleaseDetailPage) {
4242 @Shell(l, nil) {
43- <h1>{ p.Title }</h1>
44- <p>
45- <span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow)
46- if p.SHA != "" {
47- <a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a>
48- }
49- <span class="dim">{ p.Published }</span>
50- </p>
51- <div class="md">@templ.Raw(string(p.Notes))</div>
43+ <section class="mr-card">
44+ <header class="show mr-h">
45+ <h1>{ p.Title }</h1>
46+ <dl class="kv show-kv">
47+ <dt>tag</dt>
48+ <dd><span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow)</dd>
49+ if p.SHA != "" {
50+ <dt>commit</dt>
51+ <dd><a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a></dd>
52+ }
53+ <dt>published</dt>
54+ <dd>{ p.Published }</dd>
55+ </dl>
56+ </header>
57+ <article class="mr-desc prose">@templ.Raw(string(p.Notes))</article>
58+ </section>
5259 <h2>Assets</h2>
5360 if len(p.Assets) == 0 {
5461 <p class="dim">no assets.</p>
@@ -40,15 +40,22 @@ templ Releases(l Layout, p ReleaseListPage) {
40 40
41 templ ReleaseDetail(l Layout, p ReleaseDetailPage) {41 templ ReleaseDetail(l Layout, p ReleaseDetailPage) {
42 @Shell(l, nil) {42 @Shell(l, nil) {
43- <h1>{ p.Title }</h1>43+ <section class="mr-card">
44- <p>44+ <header class="show mr-h">
45- <span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow)45+ <h1>{ p.Title }</h1>
46- if p.SHA != "" {46+ <dl class="kv show-kv">
47- <a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a>47+ <dt>tag</dt>
48- }48+ <dd><span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow)</dd>
49- <span class="dim">{ p.Published }</span>49+ if p.SHA != "" {
50- </p>50+ <dt>commit</dt>
51- <div class="md">@templ.Raw(string(p.Notes))</div>51+ <dd><a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a></dd>
52+ }
53+ <dt>published</dt>
54+ <dd>{ p.Published }</dd>
55+ </dl>
56+ </header>
57+ <article class="mr-desc prose">@templ.Raw(string(p.Notes))</article>
58+ </section>
52 <h2>Assets</h2>59 <h2>Assets</h2>
53 if len(p.Assets) == 0 {60 if len(p.Assets) == 0 {
54 <p class="dim">no assets.</p>61 <p class="dim">no assets.</p>
M internal/adapters/web/releases_templ.go +32 -28
M internal/adapters/web/releases_test.go +12 -2
M internal/adapters/web/repo.go +4 -4
M internal/adapters/web/shell.templ +2 -3
M internal/adapters/web/shell_templ.go +48 -61
M internal/adapters/web/static/js/ermite.js +13 -4
M internal/adapters/web/static/js/mr.js +2 -2
M internal/adapters/web/summary.go +21 -0
A internal/adapters/web/summary_releases_test.go +49 -0
M internal/adapters/web/view.go +7 -1
M internal/app/testdata/web/blob-binary.html +33 -30
M internal/app/testdata/web/blob-code.html +33 -30
M internal/app/testdata/web/blob-empty.html +33 -30
M internal/app/testdata/web/blob-image.html +33 -30
M internal/app/testdata/web/blob-markdown.html +33 -30
M internal/app/testdata/web/blob-svg.html +33 -30
M internal/app/testdata/web/blob-text.html +33 -30
M internal/app/testdata/web/blob-too-large.html +33 -30
M internal/app/testdata/web/blob-trojan.html +33 -30
M internal/app/testdata/web/commit-big.html +33 -30
M internal/app/testdata/web/commit-binary.html +33 -30
M internal/app/testdata/web/commit-merge.html +33 -30
M internal/app/testdata/web/commit-modify-split.html +33 -30
M internal/app/testdata/web/commit-modify.html +33 -30
M internal/app/testdata/web/commit-rename.html +33 -30
M internal/app/testdata/web/commit-root.html +33 -30
M internal/app/testdata/web/commit-trojan.html +33 -30
M internal/app/testdata/web/commits-page2.html +33 -30
M internal/app/testdata/web/commits-path.html +33 -30
M internal/app/testdata/web/commits-root.html +33 -30
M internal/app/testdata/web/commits-small.html +33 -30
M internal/app/testdata/web/empty.html +12 -9
M internal/app/testdata/web/home.html +12 -9
M internal/app/testdata/web/not-found-page.html +12 -9
M internal/app/testdata/web/not-found.html +12 -9
M internal/app/testdata/web/refs.html +33 -30
M internal/app/testdata/web/summary.html +33 -30
M internal/app/testdata/web/tree-nested.html +33 -30
M internal/app/testdata/web/tree-root.html +33 -30
M internal/app/testdata/web/tree-slash-ref.html +33 -30
M internal/app/testdata/web/tree-subdir.html +33 -30
M internal/app/web_issue_test.go +1 -1
M internal/app/web_mr_test.go +1 -1
M internal/ports/mocks/ReleaseStore.go +66 -0
M internal/ports/release.go +3 -0
M internal/usecase/releases.go +25 -9
M internal/usecase/releases_integration_test.go +59 -0
A internal/usecase/releases_write_test.go +27 -0

Comments0 open / 0

no comments yet.

Keyboard shortcuts

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

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

Branches 2

Tags 41

↑↓ moveenter openesc close