feat(web): summary doc tabs, license and release badges on the index, keyboard fixes (!34)
- feat(web): document tabs and main-content navigation on the summary - fix(web): select the summary doc by exact file name first - feat(web): show cached repository licenses and latest release badges - fix(web): new activity pill expires after a minute on the index - feat(web): small refresh icon instead of the new activity pill, expires after a minute - style(web): CI mark alone on the index, state in the tooltip - style(web): smaller badge text - merge(web): combine summary docs and index badges - fix(web): refresh the index row on release events - fix(web): tell MIT-0 from MIT in license detection - test(web): mock the activity timer in the a11y test - test(web): summary golden with the labelled document card and no rail cursor - chore(web): mark renderDoc as display-only for hexcheck - style(web): link pills without underline, lighter border on hover - fix(web): keep the row hover underline off the name pills - fix(web): count ids and entry overhead in the path and license cache budget
27 files · +1694 -728
M internal/adapters/web/ASSETS.md +1 -1
| @@ -19,7 +19,7 @@ content-hashed names. Nothing is fetched at runtime. | |||
| 19 | 19 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | |
| 20 | 20 | | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` | | |
| 21 | 21 | | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` | | |
| 22 | - | | `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is. Only the icons in use are copied, one file each: `branch`, `search`, `tag`, `file`, `chevron`, `user`, `key`, `log-in`, `log-out`, `cards`, `split`, `compact`, `sun-moon`, `sun`, `moon` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`, `rows-2`, `columns-2`, `list`, `sun-moon`, `sun`, `moon`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | | |
| 22 | + | | `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is. Only the icons in use are copied, one file each: `branch`, `search`, `tag`, `file`, `chevron`, `user`, `key`, `log-in`, `log-out`, `cards`, `split`, `compact`, `refresh-cw`, `sun-moon`, `sun`, `moon` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`, `rows-2`, `columns-2`, `list`, `refresh-cw`, `sun-moon`, `sun`, `moon`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | | |
| 23 | 23 | | `static/app/*.png` | icons of the installed web app (manifest, `apple-touch-icon`), rendered from `brand.svg` with resvg; the maskable and Apple icons have the picture in the safe zone on `#181818` | generated here | | | |
| 24 | 24 | | `static/sw.js` | the service worker of the installed app: a network-only passthrough (no cache), served at `/-/sw.js` with `Service-Worker-Allowed: /` and revalidated on each check (not a hashed asset) | written here | | | |
| 25 | 25 | | `static/rules.json` | speculation rules: prefetch the light same-origin pages on hover (`Speculation-Rules` header); the heavy ones (tree, blob, commit, commits, compare, refs, raw, MR "since") are excluded, each costs several Git reads on the server | written here | | | |
| @@ -19,7 +19,7 @@ content-hashed names. Nothing is fetched at runtime. | |||||
| 19 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | 19 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | ||
| 20 | | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` | | 20 | | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` | | ||
| 21 | | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` | | 21 | | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` | | ||
| 22 | - | | `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is. Only the icons in use are copied, one file each: `branch`, `search`, `tag`, `file`, `chevron`, `user`, `key`, `log-in`, `log-out`, `cards`, `split`, `compact`, `sun-moon`, `sun`, `moon` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`, `rows-2`, `columns-2`, `list`, `sun-moon`, `sun`, `moon`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | | 22 | + | | `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is. Only the icons in use are copied, one file each: `branch`, `search`, `tag`, `file`, `chevron`, `user`, `key`, `log-in`, `log-out`, `cards`, `split`, `compact`, `refresh-cw`, `sun-moon`, `sun`, `moon` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `chevron-down`, `user-round`, `key-round`, `log-in`, `log-out`, `rows-2`, `columns-2`, `list`, `refresh-cw`, `sun-moon`, `sun`, `moon`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | | |
| 23 | | `static/app/*.png` | icons of the installed web app (manifest, `apple-touch-icon`), rendered from `brand.svg` with resvg; the maskable and Apple icons have the picture in the safe zone on `#181818` | generated here | | | 23 | | `static/app/*.png` | icons of the installed web app (manifest, `apple-touch-icon`), rendered from `brand.svg` with resvg; the maskable and Apple icons have the picture in the safe zone on `#181818` | generated here | | | ||
| 24 | | `static/sw.js` | the service worker of the installed app: a network-only passthrough (no cache), served at `/-/sw.js` with `Service-Worker-Allowed: /` and revalidated on each check (not a hashed asset) | written here | | | 24 | | `static/sw.js` | the service worker of the installed app: a network-only passthrough (no cache), served at `/-/sw.js` with `Service-Worker-Allowed: /` and revalidated on each check (not a hashed asset) | written here | | | ||
| 25 | | `static/rules.json` | speculation rules: prefetch the light same-origin pages on hover (`Speculation-Rules` header); the heavy ones (tree, blob, commit, commits, compare, refs, raw, MR "since") are excluded, each costs several Git reads on the server | written here | | | 25 | | `static/rules.json` | speculation rules: prefetch the light same-origin pages on hover (`Speculation-Rules` header); the heavy ones (tree, blob, commit, commits, compare, refs, raw, MR "since") are excluded, each costs several Git reads on the server | written here | | | ||
M internal/adapters/web/components.templ +27 -9
| @@ -41,11 +41,17 @@ templ HomeRowView(r HomeRow) { | |||
| 41 | 41 | <div class="repo-main"> | |
| 42 | 42 | <h2 class="repo-name"> | |
| 43 | 43 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | |
| 44 | + | if rel := r.Release; rel != nil { | |
| 45 | + | <a class="pill repo-release" href={ templ.SafeURL(rel.Href) } title={ "Latest release: " + rel.Tag }>{ rel.Tag }</a> | |
| 46 | + | } | |
| 44 | 47 | if r.Private { | |
| 45 | 48 | <span class="pill">private</span> | |
| 46 | 49 | } | |
| 50 | + | if lic := r.License; lic != nil { | |
| 51 | + | <a class="pill repo-license" href={ templ.SafeURL(lic.Href) } title={ "License: " + lic.ID }>{ lic.ID }</a> | |
| 52 | + | } | |
| 47 | 53 | if r.Fresh { | |
| 48 | - | <span class="pill new">new activity</span> | |
| 54 | + | <span class="repo-activity" title="new activity"><span class="ico i-refresh-cw" aria-hidden="true"></span><span class="sr">new activity</span></span> | |
| 49 | 55 | } | |
| 50 | 56 | </h2> | |
| 51 | 57 | if r.Desc != "" { | |
| @@ -94,9 +100,9 @@ templ HomeRowView(r HomeRow) { | |||
| 94 | 100 | } | |
| 95 | 101 | </dl> | |
| 96 | 102 | if r.CI != nil { | |
| 97 | - | <a class="repo-ci" href={ templ.SafeURL(r.CI.Href) } title={ r.CI.Title }> | |
| 103 | + | <a class="repo-ci" href={ templ.SafeURL(r.CI.Href) } title={ r.CI.Label + " · " + r.CI.Title }> | |
| 98 | 104 | <span class={ "ck", "ck-" + r.CI.Kind } aria-hidden="true"></span> | |
| 99 | - | <span class="sr">CI: </span><span class="ci-label">{ r.CI.Label }</span> | |
| 105 | + | <span class="sr">{ "CI: " + r.CI.Label }</span> | |
| 100 | 106 | </a> | |
| 101 | 107 | } | |
| 102 | 108 | </div> | |
| @@ -153,12 +159,24 @@ templ Outline(headings []Heading) { | |||
| 153 | 159 | } | |
| 154 | 160 | ||
| 155 | 161 | // ReadmeCard is a README in a card whose header names the file. | |
| 156 | - | templ ReadmeCard(r Readme) { | |
| 157 | - | <section class="card readme" aria-labelledby="readme-h"> | |
| 158 | - | <h2 class="card-h" id="readme-h"> | |
| 159 | - | <span class="ico i-file" aria-hidden="true"></span> | |
| 160 | - | { r.Name } | |
| 161 | - | </h2> | |
| 162 | + | templ ReadmeCard(r Readme, docs ...DocTab) { | |
| 163 | + | <section class="card readme" aria-label={ r.Name }> | |
| 164 | + | if len(docs) > 1 { | |
| 165 | + | <nav class="card-h doc-tabs" aria-label="Repository documents"> | |
| 166 | + | for _, doc := range docs { | |
| 167 | + | <a href={ templ.SafeURL(doc.Href) } | |
| 168 | + | if doc.Current { | |
| 169 | + | aria-current="page" | |
| 170 | + | } | |
| 171 | + | >{ doc.Name }</a> | |
| 172 | + | } | |
| 173 | + | </nav> | |
| 174 | + | } else { | |
| 175 | + | <h2 class="card-h" id="readme-h"> | |
| 176 | + | <span class="ico i-file" aria-hidden="true"></span> | |
| 177 | + | { r.Name } | |
| 178 | + | </h2> | |
| 179 | + | } | |
| 162 | 180 | @ReadmeView(r) | |
| 163 | 181 | </section> | |
| 164 | 182 | } | |
| @@ -41,11 +41,17 @@ templ HomeRowView(r HomeRow) { | |||||
| 41 | <div class="repo-main"> | 41 | <div class="repo-main"> | ||
| 42 | <h2 class="repo-name"> | 42 | <h2 class="repo-name"> | ||
| 43 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | 43 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | ||
| 44 | + | if rel := r.Release; rel != nil { | |||
| 45 | + | <a class="pill repo-release" href={ templ.SafeURL(rel.Href) } title={ "Latest release: " + rel.Tag }>{ rel.Tag }</a> | |||
| 46 | + | } | |||
| 44 | if r.Private { | 47 | if r.Private { | ||
| 45 | <span class="pill">private</span> | 48 | <span class="pill">private</span> | ||
| 46 | } | 49 | } | ||
| 50 | + | if lic := r.License; lic != nil { | |||
| 51 | + | <a class="pill repo-license" href={ templ.SafeURL(lic.Href) } title={ "License: " + lic.ID }>{ lic.ID }</a> | |||
| 52 | + | } | |||
| 47 | if r.Fresh { | 53 | if r.Fresh { | ||
| 48 | - | <span class="pill new">new activity</span> | 54 | + | <span class="repo-activity" title="new activity"><span class="ico i-refresh-cw" aria-hidden="true"></span><span class="sr">new activity</span></span> |
| 49 | } | 55 | } | ||
| 50 | </h2> | 56 | </h2> | ||
| 51 | if r.Desc != "" { | 57 | if r.Desc != "" { | ||
| @@ -94,9 +100,9 @@ templ HomeRowView(r HomeRow) { | |||||
| 94 | } | 100 | } | ||
| 95 | </dl> | 101 | </dl> | ||
| 96 | if r.CI != nil { | 102 | if r.CI != nil { | ||
| 97 | - | <a class="repo-ci" href={ templ.SafeURL(r.CI.Href) } title={ r.CI.Title }> | 103 | + | <a class="repo-ci" href={ templ.SafeURL(r.CI.Href) } title={ r.CI.Label + " · " + r.CI.Title }> |
| 98 | <span class={ "ck", "ck-" + r.CI.Kind } aria-hidden="true"></span> | 104 | <span class={ "ck", "ck-" + r.CI.Kind } aria-hidden="true"></span> | ||
| 99 | - | <span class="sr">CI: </span><span class="ci-label">{ r.CI.Label }</span> | 105 | + | <span class="sr">{ "CI: " + r.CI.Label }</span> |
| 100 | </a> | 106 | </a> | ||
| 101 | } | 107 | } | ||
| 102 | </div> | 108 | </div> | ||
| @@ -153,12 +159,24 @@ templ Outline(headings []Heading) { | |||||
| 153 | } | 159 | } | ||
| 154 | 160 | ||||
| 155 | // ReadmeCard is a README in a card whose header names the file. | 161 | // ReadmeCard is a README in a card whose header names the file. | ||
| 156 | - | templ ReadmeCard(r Readme) { | 162 | + | templ ReadmeCard(r Readme, docs ...DocTab) { |
| 157 | - | <section class="card readme" aria-labelledby="readme-h"> | 163 | + | <section class="card readme" aria-label={ r.Name }> |
| 158 | - | <h2 class="card-h" id="readme-h"> | 164 | + | if len(docs) > 1 { |
| 159 | - | <span class="ico i-file" aria-hidden="true"></span> | 165 | + | <nav class="card-h doc-tabs" aria-label="Repository documents"> |
| 160 | - | { r.Name } | 166 | + | for _, doc := range docs { |
| 161 | - | </h2> | 167 | + | <a href={ templ.SafeURL(doc.Href) } |
| 168 | + | if doc.Current { | |||
| 169 | + | aria-current="page" | |||
| 170 | + | } | |||
| 171 | + | >{ doc.Name }</a> | |||
| 172 | + | } | |||
| 173 | + | </nav> | |||
| 174 | + | } else { | |||
| 175 | + | <h2 class="card-h" id="readme-h"> | |||
| 176 | + | <span class="ico i-file" aria-hidden="true"></span> | |||
| 177 | + | { r.Name } | |||
| 178 | + | </h2> | |||
| 179 | + | } | |||
| 162 | @ReadmeView(r) | 180 | @ReadmeView(r) | ||
| 163 | </section> | 181 | </section> | ||
| 164 | } | 182 | } | ||
M internal/adapters/web/components_templ.go +504 -344 generated file: collapsed
M internal/adapters/web/fragments.go +8 -0
| @@ -135,6 +135,14 @@ func (l *liveStream) home(ctx context.Context, we ports.WatchEvent) ([]fragment, | |||
| 135 | 135 | name, fresh = ev.Issue.Repo.String(), true | |
| 136 | 136 | case domain.IssueCommented: | |
| 137 | 137 | name, fresh = ev.Repo.String(), true | |
| 138 | + | case domain.ReleasePublished: | |
| 139 | + | name = ev.Release.Repo.String() | |
| 140 | + | case domain.ReleaseUpdated: | |
| 141 | + | name = ev.Release.Repo.String() | |
| 142 | + | case domain.ReleaseDeleted: | |
| 143 | + | name = ev.Release.Repo.String() | |
| 144 | + | case domain.ReleaseUnpublished: | |
| 145 | + | name = ev.Release.Repo.String() | |
| 138 | 146 | case domain.RunUpdated: | |
| 139 | 147 | name = ev.Run.Repo.String() // the CI state of the row | |
| 140 | 148 | case domain.JobUpdated: | |
| @@ -135,6 +135,14 @@ func (l *liveStream) home(ctx context.Context, we ports.WatchEvent) ([]fragment, | |||||
| 135 | name, fresh = ev.Issue.Repo.String(), true | 135 | name, fresh = ev.Issue.Repo.String(), true | ||
| 136 | case domain.IssueCommented: | 136 | case domain.IssueCommented: | ||
| 137 | name, fresh = ev.Repo.String(), true | 137 | name, fresh = ev.Repo.String(), true | ||
| 138 | + | case domain.ReleasePublished: | |||
| 139 | + | name = ev.Release.Repo.String() | |||
| 140 | + | case domain.ReleaseUpdated: | |||
| 141 | + | name = ev.Release.Repo.String() | |||
| 142 | + | case domain.ReleaseDeleted: | |||
| 143 | + | name = ev.Release.Repo.String() | |||
| 144 | + | case domain.ReleaseUnpublished: | |||
| 145 | + | name = ev.Release.Repo.String() | |||
| 138 | case domain.RunUpdated: | 146 | case domain.RunUpdated: | ||
| 139 | name = ev.Run.Repo.String() // the CI state of the row | 147 | name = ev.Run.Repo.String() // the CI state of the row | ||
| 140 | case domain.JobUpdated: | 148 | case domain.JobUpdated: | ||
M internal/adapters/web/home.go +2 -0
| @@ -162,6 +162,7 @@ func (s *Server) homeRows(ctx context.Context, repos []ports.RepoView, now time. | |||
| 162 | 162 | // the newest commits of its default branch and the state of its latest run. | |
| 163 | 163 | func (s *Server) homeRowOf(ctx context.Context, r ports.RepoView, now time.Time, cursor bool) HomeRow { | |
| 164 | 164 | row := homeRow(r, now, cursor, SignedIn(ctx)) | |
| 165 | + | row.Release = s.summaryRelease(ctx, r.Name, now) | |
| 165 | 166 | if r.PushedAt.IsZero() || r.DefaultBranch == "" { | |
| 166 | 167 | row.NoCommits = r.PushedAt.IsZero() | |
| 167 | 168 | return row | |
| @@ -169,6 +170,7 @@ func (s *Server) homeRowOf(ctx context.Context, r ports.RepoView, now time.Time, | |||
| 169 | 170 | var head string | |
| 170 | 171 | row.Commits, head, row.NoCommits = s.homeCommits(ctx, r, now) | |
| 171 | 172 | if head != "" { | |
| 173 | + | row.License = s.repositoryLicense(ctx, r.Name, head) | |
| 172 | 174 | row.CI = s.homeCI(ctx, r, head) | |
| 173 | 175 | row.Stats = s.homeStats(ctx, r, head, now) | |
| 174 | 176 | } | |
| @@ -162,6 +162,7 @@ func (s *Server) homeRows(ctx context.Context, repos []ports.RepoView, now time. | |||||
| 162 | // the newest commits of its default branch and the state of its latest run. | 162 | // the newest commits of its default branch and the state of its latest run. | ||
| 163 | func (s *Server) homeRowOf(ctx context.Context, r ports.RepoView, now time.Time, cursor bool) HomeRow { | 163 | func (s *Server) homeRowOf(ctx context.Context, r ports.RepoView, now time.Time, cursor bool) HomeRow { | ||
| 164 | row := homeRow(r, now, cursor, SignedIn(ctx)) | 164 | row := homeRow(r, now, cursor, SignedIn(ctx)) | ||
| 165 | + | row.Release = s.summaryRelease(ctx, r.Name, now) | |||
| 165 | if r.PushedAt.IsZero() || r.DefaultBranch == "" { | 166 | if r.PushedAt.IsZero() || r.DefaultBranch == "" { | ||
| 166 | row.NoCommits = r.PushedAt.IsZero() | 167 | row.NoCommits = r.PushedAt.IsZero() | ||
| 167 | return row | 168 | return row | ||
| @@ -169,6 +170,7 @@ func (s *Server) homeRowOf(ctx context.Context, r ports.RepoView, now time.Time, | |||||
| 169 | var head string | 170 | var head string | ||
| 170 | row.Commits, head, row.NoCommits = s.homeCommits(ctx, r, now) | 171 | row.Commits, head, row.NoCommits = s.homeCommits(ctx, r, now) | ||
| 171 | if head != "" { | 172 | if head != "" { | ||
| 173 | + | row.License = s.repositoryLicense(ctx, r.Name, head) | |||
| 172 | row.CI = s.homeCI(ctx, r, head) | 174 | row.CI = s.homeCI(ctx, r, head) | ||
| 173 | row.Stats = s.homeStats(ctx, r, head, now) | 175 | row.Stats = s.homeStats(ctx, r, head, now) | ||
| 174 | } | 176 | } | ||
M internal/adapters/web/home_test.go +81 -2
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "context" | |
| 8 | 8 | "errors" | |
| 9 | 9 | "fmt" | |
| 10 | + | "html" | |
| 10 | 11 | "net/http" | |
| 11 | 12 | "net/http/httptest" | |
| 12 | 13 | "strings" | |
| @@ -50,6 +51,7 @@ func homeCommits(t testing.TB, n int) []domain.Commit { | |||
| 50 | 51 | func homeBrowse(t *testing.T) *mocks.BrowseService { | |
| 51 | 52 | t.Helper() | |
| 52 | 53 | b := mocks.NewBrowseService(t) | |
| 54 | + | b.EXPECT().GetTree(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, false).Return(domain.Tree{}, nil).Maybe() | |
| 53 | 55 | b.EXPECT().RepoStats(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). | |
| 54 | 56 | Return(ports.RepoStats{Commits: 1234, Recent: 7}, nil).Maybe() | |
| 55 | 57 | return b | |
| @@ -63,6 +65,7 @@ func TestHomeRowFigures(t *testing.T) { | |||
| 63 | 65 | head := objectID(t, fmt.Sprintf("%040x", 2)) | |
| 64 | 66 | now := time.Now() | |
| 65 | 67 | browse := mocks.NewBrowseService(t) | |
| 68 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, false).Return(domain.Tree{}, nil).Maybe() | |
| 66 | 69 | browse.EXPECT().ListCommits(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.HistoryPage{Commits: homeCommits(t, 2)}, nil) | |
| 67 | 70 | browse.EXPECT().RepoStats(mock.Anything, mock.Anything, repo, head, now).Return(ports.RepoStats{ | |
| 68 | 71 | Commits: 12345, Recent: 42, LatestTag: &domain.TagInfo{Name: "v1.2.0", At: now.Add(-3 * time.Hour), CommitID: head}, | |
| @@ -212,8 +215,10 @@ func TestHomeRowCI(t *testing.T) { | |||
| 212 | 215 | } | |
| 213 | 216 | require.NotNil(t, row.CI) | |
| 214 | 217 | assert.Contains(t, out, `class="repo-ci" href="`+tt.href+`"`) | |
| 215 | - | assert.Contains(t, out, `<span class="ci-label">`+tt.label+`</span>`, "the state has a word, not only a mark; compact hides it") | |
| 216 | - | assert.Contains(t, out, `<span class="sr">CI: </span>`) | |
| 218 | + | assert.Contains(t, out, `<span class="sr">CI: `+tt.label+`</span>`) | |
| 219 | + | assert.NotContains(t, out, `class="ci-label"`) | |
| 220 | + | assert.Contains(t, out, `title="`+html.EscapeString(tt.label+" · "+row.CI.Title)+`"`) | |
| 221 | + | assert.Contains(t, out, `class="ck ck-`+row.CI.Kind+`" aria-hidden="true"`) | |
| 217 | 222 | }) | |
| 218 | 223 | } | |
| 219 | 224 | } | |
| @@ -393,3 +398,77 @@ func TestActivityEventsRefreshTheHomeRow(t *testing.T) { | |||
| 393 | 398 | assert.Empty(t, got) | |
| 394 | 399 | } | |
| 395 | 400 | } | |
| 401 | + | ||
| 402 | + | func TestHomeReleaseAndLicensePills(t *testing.T) { | |
| 403 | + | t.Parallel() | |
| 404 | + | for _, tc := range []struct { | |
| 405 | + | name string | |
| 406 | + | release domain.Release | |
| 407 | + | err error | |
| 408 | + | visible bool | |
| 409 | + | }{ | |
| 410 | + | {name: "official release", release: domain.Release{Tag: "v1.2.0", PublishedAt: fixedNow}, visible: true}, | |
| 411 | + | {name: "plain tag only", err: domain.ErrNotFound}, | |
| 412 | + | {name: "draft", release: domain.Release{Tag: "secret", Draft: true}}, | |
| 413 | + | {name: "unpublished", release: domain.Release{Tag: "pending"}}, | |
| 414 | + | } { | |
| 415 | + | t.Run(tc.name, func(t *testing.T) { | |
| 416 | + | t.Parallel() | |
| 417 | + | repo := publicRepo(t, "proj") | |
| 418 | + | repo.Visibility = domain.VisibilityPrivate | |
| 419 | + | svc := mocks.NewReleaseService(t) | |
| 420 | + | svc.EXPECT().GetRelease(mock.Anything, mock.Anything, repo.Name, ports.ReleaseRef{Latest: true}).Return(tc.release, tc.err).Once() | |
| 421 | + | srv := newTestServer(t, Config{Repos: reposServing(t, repo), Browse: mocks.NewBrowseService(t), Releases: svc}) | |
| 422 | + | row := srv.homeRowOf(t.Context(), repo, fixedNow, false) | |
| 423 | + | row.Private, row.Fresh = true, true | |
| 424 | + | row.License = &LicenseBadge{ID: "MIT", Href: "/proj/blob/main/LICENSE"} | |
| 425 | + | out := renderRow(t, row) | |
| 426 | + | assert.Contains(t, out, `class="pill repo-license" href="/proj/blob/main/LICENSE" title="License: MIT">MIT</a>`) | |
| 427 | + | assert.Regexp(t, `(?s)>private</span>.*repo-license.*class="repo-activity" title="new activity"><span class="ico i-refresh-cw" aria-hidden="true"></span><span class="sr">new activity</span></span></h2>`, out) | |
| 428 | + | if tc.visible { | |
| 429 | + | assert.Regexp(t, `(?s)>proj</a>.*class="pill repo-release".*>v1.2.0</a>.*>private</span>`, out) | |
| 430 | + | assert.Contains(t, out, `href="/proj/releases/tag/v1.2.0"`) | |
| 431 | + | } else { | |
| 432 | + | assert.NotContains(t, out, `repo-release`) | |
| 433 | + | } | |
| 434 | + | }) | |
| 435 | + | } | |
| 436 | + | out := renderRow(t, HomeRow{Name: "bare"}) | |
| 437 | + | assert.NotContains(t, out, `repo-license`) | |
| 438 | + | assert.NotContains(t, out, `repo-release`) | |
| 439 | + | } | |
| 440 | + | ||
| 441 | + | func TestReleaseEventsRefreshTheHomeRow(t *testing.T) { | |
| 442 | + | t.Parallel() | |
| 443 | + | repo := publicRepo(t, "proj") | |
| 444 | + | rel := domain.Release{Repo: repo.Name, Tag: "v2", PublishedAt: fixedNow} | |
| 445 | + | for _, tc := range []struct { | |
| 446 | + | event domain.Event | |
| 447 | + | latest domain.Release | |
| 448 | + | err error | |
| 449 | + | }{ | |
| 450 | + | {domain.ReleasePublished{Release: rel}, rel, nil}, | |
| 451 | + | {domain.ReleaseUpdated{Release: rel}, domain.Release{Tag: "v1", PublishedAt: fixedNow}, nil}, | |
| 452 | + | {domain.ReleaseDeleted{Release: rel}, domain.Release{}, domain.ErrNotFound}, | |
| 453 | + | {domain.ReleaseUnpublished{Release: rel}, domain.Release{}, domain.ErrNotFound}, | |
| 454 | + | } { | |
| 455 | + | t.Run(tc.event.EventKind(), func(t *testing.T) { | |
| 456 | + | t.Parallel() | |
| 457 | + | releases := mocks.NewReleaseService(t) | |
| 458 | + | releases.EXPECT().GetRelease(mock.Anything, mock.Anything, repo.Name, ports.ReleaseRef{Latest: true}).Return(tc.latest, tc.err).Once() | |
| 459 | + | srv := newTestServer(t, Config{Repos: reposServing(t, repo), Browse: homeBrowse(t), Releases: releases}) | |
| 460 | + | got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: tc.event}) | |
| 461 | + | require.NoError(t, err) | |
| 462 | + | require.Len(t, got, 1) | |
| 463 | + | assert.Equal(t, "home-list", got[0].event) | |
| 464 | + | assert.Contains(t, got[0].html, `id="repo-proj"`) | |
| 465 | + | assert.NotContains(t, got[0].html, `data-activity`) | |
| 466 | + | assert.NotContains(t, got[0].html, `new activity`) | |
| 467 | + | if tc.err != nil { | |
| 468 | + | assert.NotContains(t, got[0].html, `repo-release`) | |
| 469 | + | } else { | |
| 470 | + | assert.Contains(t, got[0].html, `href="/proj/releases/tag/`+tc.latest.Tag+`"`) | |
| 471 | + | } | |
| 472 | + | }) | |
| 473 | + | } | |
| 474 | + | } | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "context" | 7 | "context" | ||
| 8 | "errors" | 8 | "errors" | ||
| 9 | "fmt" | 9 | "fmt" | ||
| 10 | + | "html" | |||
| 10 | "net/http" | 11 | "net/http" | ||
| 11 | "net/http/httptest" | 12 | "net/http/httptest" | ||
| 12 | "strings" | 13 | "strings" | ||
| @@ -50,6 +51,7 @@ func homeCommits(t testing.TB, n int) []domain.Commit { | |||||
| 50 | func homeBrowse(t *testing.T) *mocks.BrowseService { | 51 | func homeBrowse(t *testing.T) *mocks.BrowseService { | ||
| 51 | t.Helper() | 52 | t.Helper() | ||
| 52 | b := mocks.NewBrowseService(t) | 53 | b := mocks.NewBrowseService(t) | ||
| 54 | + | b.EXPECT().GetTree(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, false).Return(domain.Tree{}, nil).Maybe() | |||
| 53 | b.EXPECT().RepoStats(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). | 55 | b.EXPECT().RepoStats(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). | ||
| 54 | Return(ports.RepoStats{Commits: 1234, Recent: 7}, nil).Maybe() | 56 | Return(ports.RepoStats{Commits: 1234, Recent: 7}, nil).Maybe() | ||
| 55 | return b | 57 | return b | ||
| @@ -63,6 +65,7 @@ func TestHomeRowFigures(t *testing.T) { | |||||
| 63 | head := objectID(t, fmt.Sprintf("%040x", 2)) | 65 | head := objectID(t, fmt.Sprintf("%040x", 2)) | ||
| 64 | now := time.Now() | 66 | now := time.Now() | ||
| 65 | browse := mocks.NewBrowseService(t) | 67 | browse := mocks.NewBrowseService(t) | ||
| 68 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, false).Return(domain.Tree{}, nil).Maybe() | |||
| 66 | browse.EXPECT().ListCommits(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.HistoryPage{Commits: homeCommits(t, 2)}, nil) | 69 | browse.EXPECT().ListCommits(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.HistoryPage{Commits: homeCommits(t, 2)}, nil) | ||
| 67 | browse.EXPECT().RepoStats(mock.Anything, mock.Anything, repo, head, now).Return(ports.RepoStats{ | 70 | browse.EXPECT().RepoStats(mock.Anything, mock.Anything, repo, head, now).Return(ports.RepoStats{ | ||
| 68 | Commits: 12345, Recent: 42, LatestTag: &domain.TagInfo{Name: "v1.2.0", At: now.Add(-3 * time.Hour), CommitID: head}, | 71 | Commits: 12345, Recent: 42, LatestTag: &domain.TagInfo{Name: "v1.2.0", At: now.Add(-3 * time.Hour), CommitID: head}, | ||
| @@ -212,8 +215,10 @@ func TestHomeRowCI(t *testing.T) { | |||||
| 212 | } | 215 | } | ||
| 213 | require.NotNil(t, row.CI) | 216 | require.NotNil(t, row.CI) | ||
| 214 | assert.Contains(t, out, `class="repo-ci" href="`+tt.href+`"`) | 217 | assert.Contains(t, out, `class="repo-ci" href="`+tt.href+`"`) | ||
| 215 | - | assert.Contains(t, out, `<span class="ci-label">`+tt.label+`</span>`, "the state has a word, not only a mark; compact hides it") | 218 | + | assert.Contains(t, out, `<span class="sr">CI: `+tt.label+`</span>`) |
| 216 | - | assert.Contains(t, out, `<span class="sr">CI: </span>`) | 219 | + | assert.NotContains(t, out, `class="ci-label"`) |
| 220 | + | assert.Contains(t, out, `title="`+html.EscapeString(tt.label+" · "+row.CI.Title)+`"`) | |||
| 221 | + | assert.Contains(t, out, `class="ck ck-`+row.CI.Kind+`" aria-hidden="true"`) | |||
| 217 | }) | 222 | }) | ||
| 218 | } | 223 | } | ||
| 219 | } | 224 | } | ||
| @@ -393,3 +398,77 @@ func TestActivityEventsRefreshTheHomeRow(t *testing.T) { | |||||
| 393 | assert.Empty(t, got) | 398 | assert.Empty(t, got) | ||
| 394 | } | 399 | } | ||
| 395 | } | 400 | } | ||
| 401 | + | ||||
| 402 | + | func TestHomeReleaseAndLicensePills(t *testing.T) { | |||
| 403 | + | t.Parallel() | |||
| 404 | + | for _, tc := range []struct { | |||
| 405 | + | name string | |||
| 406 | + | release domain.Release | |||
| 407 | + | err error | |||
| 408 | + | visible bool | |||
| 409 | + | }{ | |||
| 410 | + | {name: "official release", release: domain.Release{Tag: "v1.2.0", PublishedAt: fixedNow}, visible: true}, | |||
| 411 | + | {name: "plain tag only", err: domain.ErrNotFound}, | |||
| 412 | + | {name: "draft", release: domain.Release{Tag: "secret", Draft: true}}, | |||
| 413 | + | {name: "unpublished", release: domain.Release{Tag: "pending"}}, | |||
| 414 | + | } { | |||
| 415 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 416 | + | t.Parallel() | |||
| 417 | + | repo := publicRepo(t, "proj") | |||
| 418 | + | repo.Visibility = domain.VisibilityPrivate | |||
| 419 | + | svc := mocks.NewReleaseService(t) | |||
| 420 | + | svc.EXPECT().GetRelease(mock.Anything, mock.Anything, repo.Name, ports.ReleaseRef{Latest: true}).Return(tc.release, tc.err).Once() | |||
| 421 | + | srv := newTestServer(t, Config{Repos: reposServing(t, repo), Browse: mocks.NewBrowseService(t), Releases: svc}) | |||
| 422 | + | row := srv.homeRowOf(t.Context(), repo, fixedNow, false) | |||
| 423 | + | row.Private, row.Fresh = true, true | |||
| 424 | + | row.License = &LicenseBadge{ID: "MIT", Href: "/proj/blob/main/LICENSE"} | |||
| 425 | + | out := renderRow(t, row) | |||
| 426 | + | assert.Contains(t, out, `class="pill repo-license" href="/proj/blob/main/LICENSE" title="License: MIT">MIT</a>`) | |||
| 427 | + | assert.Regexp(t, `(?s)>private</span>.*repo-license.*class="repo-activity" title="new activity"><span class="ico i-refresh-cw" aria-hidden="true"></span><span class="sr">new activity</span></span></h2>`, out) | |||
| 428 | + | if tc.visible { | |||
| 429 | + | assert.Regexp(t, `(?s)>proj</a>.*class="pill repo-release".*>v1.2.0</a>.*>private</span>`, out) | |||
| 430 | + | assert.Contains(t, out, `href="/proj/releases/tag/v1.2.0"`) | |||
| 431 | + | } else { | |||
| 432 | + | assert.NotContains(t, out, `repo-release`) | |||
| 433 | + | } | |||
| 434 | + | }) | |||
| 435 | + | } | |||
| 436 | + | out := renderRow(t, HomeRow{Name: "bare"}) | |||
| 437 | + | assert.NotContains(t, out, `repo-license`) | |||
| 438 | + | assert.NotContains(t, out, `repo-release`) | |||
| 439 | + | } | |||
| 440 | + | ||||
| 441 | + | func TestReleaseEventsRefreshTheHomeRow(t *testing.T) { | |||
| 442 | + | t.Parallel() | |||
| 443 | + | repo := publicRepo(t, "proj") | |||
| 444 | + | rel := domain.Release{Repo: repo.Name, Tag: "v2", PublishedAt: fixedNow} | |||
| 445 | + | for _, tc := range []struct { | |||
| 446 | + | event domain.Event | |||
| 447 | + | latest domain.Release | |||
| 448 | + | err error | |||
| 449 | + | }{ | |||
| 450 | + | {domain.ReleasePublished{Release: rel}, rel, nil}, | |||
| 451 | + | {domain.ReleaseUpdated{Release: rel}, domain.Release{Tag: "v1", PublishedAt: fixedNow}, nil}, | |||
| 452 | + | {domain.ReleaseDeleted{Release: rel}, domain.Release{}, domain.ErrNotFound}, | |||
| 453 | + | {domain.ReleaseUnpublished{Release: rel}, domain.Release{}, domain.ErrNotFound}, | |||
| 454 | + | } { | |||
| 455 | + | t.Run(tc.event.EventKind(), func(t *testing.T) { | |||
| 456 | + | t.Parallel() | |||
| 457 | + | releases := mocks.NewReleaseService(t) | |||
| 458 | + | releases.EXPECT().GetRelease(mock.Anything, mock.Anything, repo.Name, ports.ReleaseRef{Latest: true}).Return(tc.latest, tc.err).Once() | |||
| 459 | + | srv := newTestServer(t, Config{Repos: reposServing(t, repo), Browse: homeBrowse(t), Releases: releases}) | |||
| 460 | + | got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: tc.event}) | |||
| 461 | + | require.NoError(t, err) | |||
| 462 | + | require.Len(t, got, 1) | |||
| 463 | + | assert.Equal(t, "home-list", got[0].event) | |||
| 464 | + | assert.Contains(t, got[0].html, `id="repo-proj"`) | |||
| 465 | + | assert.NotContains(t, got[0].html, `data-activity`) | |||
| 466 | + | assert.NotContains(t, got[0].html, `new activity`) | |||
| 467 | + | if tc.err != nil { | |||
| 468 | + | assert.NotContains(t, got[0].html, `repo-release`) | |||
| 469 | + | } else { | |||
| 470 | + | assert.Contains(t, got[0].html, `href="/proj/releases/tag/`+tc.latest.Tag+`"`) | |||
| 471 | + | } | |||
| 472 | + | }) | |||
| 473 | + | } | |||
| 474 | + | } | |||
M internal/adapters/web/jstest/a11y.test.mjs +2 -1
| @@ -339,7 +339,8 @@ test('the preference is read from storage when the page loads', async () => { | |||
| 339 | 339 | }); | |
| 340 | 340 | ||
| 341 | 341 | ||
| 342 | - | test('activity moves a home row up only in newest activity order, preserving focus and cursor', () => { | |
| 342 | + | test('activity moves a home row up only in newest activity order, preserving focus and cursor', (t) => { | |
| 343 | + | t.mock.timers.enable({ apis: ['setTimeout'] }); | |
| 343 | 344 | for (const [sort, desc, activity, moves] of [ | |
| 344 | 345 | ['activity', 'true', true, true], | |
| 345 | 346 | ['activity', 'false', true, false], | |
| @@ -339,7 +339,8 @@ test('the preference is read from storage when the page loads', async () => { | |||||
| 339 | }); | 339 | }); | ||
| 340 | 340 | ||||
| 341 | 341 | ||||
| 342 | - | test('activity moves a home row up only in newest activity order, preserving focus and cursor', () => { | 342 | + | test('activity moves a home row up only in newest activity order, preserving focus and cursor', (t) => { |
| 343 | + | t.mock.timers.enable({ apis: ['setTimeout'] }); | |||
| 343 | for (const [sort, desc, activity, moves] of [ | 344 | for (const [sort, desc, activity, moves] of [ | ||
| 344 | ['activity', 'true', true, true], | 345 | ['activity', 'true', true, true], | ||
| 345 | ['activity', 'false', true, false], | 346 | ['activity', 'false', true, false], | ||
M internal/adapters/web/jstest/live.test.mjs +49 -0
| @@ -79,3 +79,52 @@ test('replacement stream URL carries the last connection id, including id-only e | |||
| 79 | 79 | assert.equal(url.searchParams.get('topic'), 'repo:demo'); | |
| 80 | 80 | } finally { globalThis.location = saved; } | |
| 81 | 81 | }); | |
| 82 | + | ||
| 83 | + | test('fresh home rows expire after a minute; later swaps still prepend and get a full minute', (t) => { | |
| 84 | + | const { live } = setup(t); | |
| 85 | + | let current = null, next = null; | |
| 86 | + | let prepends = 0; | |
| 87 | + | const rows = { dataset: { sort: 'activity', desc: 'true' }, prepend(el) { | |
| 88 | + | prepends++; | |
| 89 | + | current = el; | |
| 90 | + | el.parentElement = this; | |
| 91 | + | } }; | |
| 92 | + | const fresh = () => { | |
| 93 | + | const attrs = new Set(['data-activity']); | |
| 94 | + | const pill = { removed: false, remove() { this.removed = true; } }; | |
| 95 | + | return { id: 'repo-demo', pill, parentElement: rows, | |
| 96 | + | querySelectorAll: () => [], | |
| 97 | + | hasAttribute: (name) => attrs.has(name), | |
| 98 | + | removeAttribute: (name) => attrs.delete(name), | |
| 99 | + | querySelector: (sel) => sel === '.repo-activity' && !pill.removed ? pill : null, | |
| 100 | + | replaceWith(el) { current = el; el.parentElement = rows; }, | |
| 101 | + | }; | |
| 102 | + | }; | |
| 103 | + | document.createElement = () => ({ set innerHTML(_) {}, content: { get children() { return [next]; } } }); | |
| 104 | + | document.getElementById = (id) => id === 'repo-rows' ? rows : current; | |
| 105 | + | document.querySelectorAll = () => []; | |
| 106 | + | ||
| 107 | + | next = fresh(); | |
| 108 | + | live.swap('first'); | |
| 109 | + | const first = current; | |
| 110 | + | assert.equal(prepends, 1, 'a newly added repository is prepended'); | |
| 111 | + | t.mock.timers.tick(30000); | |
| 112 | + | next = fresh(); | |
| 113 | + | live.swap('second'); | |
| 114 | + | const second = current; | |
| 115 | + | assert.equal(prepends, 2, 'later activity reorders the existing row'); | |
| 116 | + | t.mock.timers.tick(30000); | |
| 117 | + | assert.equal(first.pill.removed, true); | |
| 118 | + | assert.equal(second.pill.removed, false, 'the old timer cannot remove the new badge'); | |
| 119 | + | t.mock.timers.tick(29999); | |
| 120 | + | assert.equal(second.pill.removed, false); | |
| 121 | + | t.mock.timers.tick(1); | |
| 122 | + | assert.equal(second.pill.removed, true, 'removal also removes it from the accessibility tree'); | |
| 123 | + | assert.equal(second.hasAttribute('data-activity'), false); | |
| 124 | + | next = fresh(); | |
| 125 | + | live.swap('third'); | |
| 126 | + | assert.equal(prepends, 3, 'activity still reorders after the previous badge expired'); | |
| 127 | + | assert.equal(current.pill.removed, false); | |
| 128 | + | t.mock.timers.tick(60000); | |
| 129 | + | assert.equal(current.pill.removed, true); | |
| 130 | + | }); | |
| @@ -79,3 +79,52 @@ test('replacement stream URL carries the last connection id, including id-only e | |||||
| 79 | assert.equal(url.searchParams.get('topic'), 'repo:demo'); | 79 | assert.equal(url.searchParams.get('topic'), 'repo:demo'); | ||
| 80 | } finally { globalThis.location = saved; } | 80 | } finally { globalThis.location = saved; } | ||
| 81 | }); | 81 | }); | ||
| 82 | + | ||||
| 83 | + | test('fresh home rows expire after a minute; later swaps still prepend and get a full minute', (t) => { | |||
| 84 | + | const { live } = setup(t); | |||
| 85 | + | let current = null, next = null; | |||
| 86 | + | let prepends = 0; | |||
| 87 | + | const rows = { dataset: { sort: 'activity', desc: 'true' }, prepend(el) { | |||
| 88 | + | prepends++; | |||
| 89 | + | current = el; | |||
| 90 | + | el.parentElement = this; | |||
| 91 | + | } }; | |||
| 92 | + | const fresh = () => { | |||
| 93 | + | const attrs = new Set(['data-activity']); | |||
| 94 | + | const pill = { removed: false, remove() { this.removed = true; } }; | |||
| 95 | + | return { id: 'repo-demo', pill, parentElement: rows, | |||
| 96 | + | querySelectorAll: () => [], | |||
| 97 | + | hasAttribute: (name) => attrs.has(name), | |||
| 98 | + | removeAttribute: (name) => attrs.delete(name), | |||
| 99 | + | querySelector: (sel) => sel === '.repo-activity' && !pill.removed ? pill : null, | |||
| 100 | + | replaceWith(el) { current = el; el.parentElement = rows; }, | |||
| 101 | + | }; | |||
| 102 | + | }; | |||
| 103 | + | document.createElement = () => ({ set innerHTML(_) {}, content: { get children() { return [next]; } } }); | |||
| 104 | + | document.getElementById = (id) => id === 'repo-rows' ? rows : current; | |||
| 105 | + | document.querySelectorAll = () => []; | |||
| 106 | + | ||||
| 107 | + | next = fresh(); | |||
| 108 | + | live.swap('first'); | |||
| 109 | + | const first = current; | |||
| 110 | + | assert.equal(prepends, 1, 'a newly added repository is prepended'); | |||
| 111 | + | t.mock.timers.tick(30000); | |||
| 112 | + | next = fresh(); | |||
| 113 | + | live.swap('second'); | |||
| 114 | + | const second = current; | |||
| 115 | + | assert.equal(prepends, 2, 'later activity reorders the existing row'); | |||
| 116 | + | t.mock.timers.tick(30000); | |||
| 117 | + | assert.equal(first.pill.removed, true); | |||
| 118 | + | assert.equal(second.pill.removed, false, 'the old timer cannot remove the new badge'); | |||
| 119 | + | t.mock.timers.tick(29999); | |||
| 120 | + | assert.equal(second.pill.removed, false); | |||
| 121 | + | t.mock.timers.tick(1); | |||
| 122 | + | assert.equal(second.pill.removed, true, 'removal also removes it from the accessibility tree'); | |||
| 123 | + | assert.equal(second.hasAttribute('data-activity'), false); | |||
| 124 | + | next = fresh(); | |||
| 125 | + | live.swap('third'); | |||
| 126 | + | assert.equal(prepends, 3, 'activity still reorders after the previous badge expired'); | |||
| 127 | + | assert.equal(current.pill.removed, false); | |||
| 128 | + | t.mock.timers.tick(60000); | |||
| 129 | + | assert.equal(current.pill.removed, true); | |||
| 130 | + | }); | |||
M internal/adapters/web/jstest/rail.test.mjs +20 -0
| @@ -62,3 +62,23 @@ test('h outside the panel leaves the focus alone; j / k outside it are not taken | |||
| 62 | 62 | assert.equal(active, main); | |
| 63 | 63 | assert.equal(R.step(1), false, 'the row cursor keeps j / k'); | |
| 64 | 64 | }); | |
| 65 | + | ||
| 66 | + | test('j / k cross section and recent-commit table boundaries in document order', () => { | |
| 67 | + | const controls = ['clone', 'about', 'commit-1', 'commit-2', 'actions', 'outline'].map((id) => el('a', { id, attrs: { href: '#' + id } })); | |
| 68 | + | for (const e of controls) e.focus = () => { active = e; }; | |
| 69 | + | rail.append( | |
| 70 | + | el('section', {}, controls[0]), | |
| 71 | + | el('section', {}, controls[1]), | |
| 72 | + | el('section', {}, el('table', {}, el('tbody', { attrs: { 'data-rows': '' } }, el('tr', {}, controls[2]), el('tr', {}, controls[3])))), | |
| 73 | + | el('section', {}, controls[4]), | |
| 74 | + | el('nav', {}, controls[5]), | |
| 75 | + | ); | |
| 76 | + | active = close; | |
| 77 | + | for (const e of controls) { assert.equal(R.step(1), true); assert.equal(active, e); } | |
| 78 | + | R.step(1); | |
| 79 | + | assert.equal(active, controls.at(-1)); | |
| 80 | + | for (const e of controls.slice(0, -1).reverse()) { R.step(-1); assert.equal(active, e); } | |
| 81 | + | R.leave(); | |
| 82 | + | assert.equal(active, main); | |
| 83 | + | assert.equal(R.step(1), false); | |
| 84 | + | }); | |
| @@ -62,3 +62,23 @@ test('h outside the panel leaves the focus alone; j / k outside it are not taken | |||||
| 62 | assert.equal(active, main); | 62 | assert.equal(active, main); | ||
| 63 | assert.equal(R.step(1), false, 'the row cursor keeps j / k'); | 63 | assert.equal(R.step(1), false, 'the row cursor keeps j / k'); | ||
| 64 | }); | 64 | }); | ||
| 65 | + | ||||
| 66 | + | test('j / k cross section and recent-commit table boundaries in document order', () => { | |||
| 67 | + | const controls = ['clone', 'about', 'commit-1', 'commit-2', 'actions', 'outline'].map((id) => el('a', { id, attrs: { href: '#' + id } })); | |||
| 68 | + | for (const e of controls) e.focus = () => { active = e; }; | |||
| 69 | + | rail.append( | |||
| 70 | + | el('section', {}, controls[0]), | |||
| 71 | + | el('section', {}, controls[1]), | |||
| 72 | + | el('section', {}, el('table', {}, el('tbody', { attrs: { 'data-rows': '' } }, el('tr', {}, controls[2]), el('tr', {}, controls[3])))), | |||
| 73 | + | el('section', {}, controls[4]), | |||
| 74 | + | el('nav', {}, controls[5]), | |||
| 75 | + | ); | |||
| 76 | + | active = close; | |||
| 77 | + | for (const e of controls) { assert.equal(R.step(1), true); assert.equal(active, e); } | |||
| 78 | + | R.step(1); | |||
| 79 | + | assert.equal(active, controls.at(-1)); | |||
| 80 | + | for (const e of controls.slice(0, -1).reverse()) { R.step(-1); assert.equal(active, e); } | |||
| 81 | + | R.leave(); | |||
| 82 | + | assert.equal(active, main); | |||
| 83 | + | assert.equal(R.step(1), false); | |||
| 84 | + | }); | |||
A internal/adapters/web/jstest/summary-navigation.test.mjs +47 -0
| @@ -0,0 +1,47 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | import assert from 'node:assert/strict'; | |
| 4 | + | import { readFileSync } from 'node:fs'; | |
| 5 | + | import { test } from 'node:test'; | |
| 6 | + | import { runInNewContext } from 'node:vm'; | |
| 7 | + | import { el, installDom } from './dom.mjs'; | |
| 8 | + | ||
| 9 | + | const source = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8'); | |
| 10 | + | ||
| 11 | + | test('main navigation ignores rail rows, but still drives main tables', () => { | |
| 12 | + | const doc = installDom(); | |
| 13 | + | const main = el('main', { id: 'main' }); | |
| 14 | + | const recent = el('tr'); | |
| 15 | + | const rail = el('aside', { cls: 'rail' }, el('tbody', { attrs: { 'data-rows': '' } }, recent)); | |
| 16 | + | doc.body.append(main, rail); | |
| 17 | + | const scrolls = []; | |
| 18 | + | main.scrollBy = (_, y) => scrolls.push(['main-by', y]); | |
| 19 | + | main.scrollTo = (_, y) => scrolls.push(['main-to', y]); | |
| 20 | + | const context = { | |
| 21 | + | window: { ermiteRail: { step: () => false } }, | |
| 22 | + | $: (s) => doc.querySelector(s), | |
| 23 | + | $$: (s) => { | |
| 24 | + | const child = s.startsWith('[data-rows] > ') ? s.slice('[data-rows] > '.length) : null; | |
| 25 | + | if (child) return [...doc.querySelectorAll('[data-rows]')].flatMap((table) => table.children.filter((row) => row.matches(child))); | |
| 26 | + | return [...doc.querySelectorAll(s)]; | |
| 27 | + | }, | |
| 28 | + | body: () => doc.body, | |
| 29 | + | flash() {}, setTimeout() {}, | |
| 30 | + | scrollBy: (_, y) => scrolls.push(['page-by', y]), | |
| 31 | + | scrollTo: (_, y) => scrolls.push(['page-to', y]), | |
| 32 | + | }; | |
| 33 | + | runInNewContext(source.slice(source.indexOf(' const visibleRows ='), source.indexOf(' window.ermite = ermite;')) + '\nwindow.ermite = ermite;', context); | |
| 34 | + | const E = context.window.ermite; | |
| 35 | + | E.move(1); E.move(-1); E.edge(false); E.edge(true); | |
| 36 | + | assert.deepEqual(scrolls, [['page-by', 180], ['main-by', 180], ['page-by', -180], ['main-by', -180], ['page-to', 0], ['main-to', 0], ['page-to', 1e9], ['main-to', 1e9]]); | |
| 37 | + | assert.equal(recent.hasAttribute('data-cursor'), false); | |
| 38 | + | const first = el('tr'); | |
| 39 | + | const last = el('tr'); | |
| 40 | + | main.append(el('tbody', { attrs: { 'data-rows': '' } }, first, last)); | |
| 41 | + | E.move(1); | |
| 42 | + | assert.equal(first.hasAttribute('data-cursor'), true); | |
| 43 | + | E.edge(true); | |
| 44 | + | assert.equal(last.hasAttribute('data-cursor'), true); | |
| 45 | + | assert.equal(first.hasAttribute('data-cursor'), false); | |
| 46 | + | assert.equal(recent.hasAttribute('data-cursor'), false); | |
| 47 | + | }); | |
| @@ -0,0 +1,47 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | import assert from 'node:assert/strict'; | |||
| 4 | + | import { readFileSync } from 'node:fs'; | |||
| 5 | + | import { test } from 'node:test'; | |||
| 6 | + | import { runInNewContext } from 'node:vm'; | |||
| 7 | + | import { el, installDom } from './dom.mjs'; | |||
| 8 | + | ||||
| 9 | + | const source = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8'); | |||
| 10 | + | ||||
| 11 | + | test('main navigation ignores rail rows, but still drives main tables', () => { | |||
| 12 | + | const doc = installDom(); | |||
| 13 | + | const main = el('main', { id: 'main' }); | |||
| 14 | + | const recent = el('tr'); | |||
| 15 | + | const rail = el('aside', { cls: 'rail' }, el('tbody', { attrs: { 'data-rows': '' } }, recent)); | |||
| 16 | + | doc.body.append(main, rail); | |||
| 17 | + | const scrolls = []; | |||
| 18 | + | main.scrollBy = (_, y) => scrolls.push(['main-by', y]); | |||
| 19 | + | main.scrollTo = (_, y) => scrolls.push(['main-to', y]); | |||
| 20 | + | const context = { | |||
| 21 | + | window: { ermiteRail: { step: () => false } }, | |||
| 22 | + | $: (s) => doc.querySelector(s), | |||
| 23 | + | $$: (s) => { | |||
| 24 | + | const child = s.startsWith('[data-rows] > ') ? s.slice('[data-rows] > '.length) : null; | |||
| 25 | + | if (child) return [...doc.querySelectorAll('[data-rows]')].flatMap((table) => table.children.filter((row) => row.matches(child))); | |||
| 26 | + | return [...doc.querySelectorAll(s)]; | |||
| 27 | + | }, | |||
| 28 | + | body: () => doc.body, | |||
| 29 | + | flash() {}, setTimeout() {}, | |||
| 30 | + | scrollBy: (_, y) => scrolls.push(['page-by', y]), | |||
| 31 | + | scrollTo: (_, y) => scrolls.push(['page-to', y]), | |||
| 32 | + | }; | |||
| 33 | + | runInNewContext(source.slice(source.indexOf(' const visibleRows ='), source.indexOf(' window.ermite = ermite;')) + '\nwindow.ermite = ermite;', context); | |||
| 34 | + | const E = context.window.ermite; | |||
| 35 | + | E.move(1); E.move(-1); E.edge(false); E.edge(true); | |||
| 36 | + | assert.deepEqual(scrolls, [['page-by', 180], ['main-by', 180], ['page-by', -180], ['main-by', -180], ['page-to', 0], ['main-to', 0], ['page-to', 1e9], ['main-to', 1e9]]); | |||
| 37 | + | assert.equal(recent.hasAttribute('data-cursor'), false); | |||
| 38 | + | const first = el('tr'); | |||
| 39 | + | const last = el('tr'); | |||
| 40 | + | main.append(el('tbody', { attrs: { 'data-rows': '' } }, first, last)); | |||
| 41 | + | E.move(1); | |||
| 42 | + | assert.equal(first.hasAttribute('data-cursor'), true); | |||
| 43 | + | E.edge(true); | |||
| 44 | + | assert.equal(last.hasAttribute('data-cursor'), true); | |||
| 45 | + | assert.equal(first.hasAttribute('data-cursor'), false); | |||
| 46 | + | assert.equal(recent.hasAttribute('data-cursor'), false); | |||
| 47 | + | }); | |||
A internal/adapters/web/license.go +115 -0
| @@ -0,0 +1,115 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "encoding/json" | |
| 8 | + | "strings" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/zerowrap" | |
| 11 | + | ||
| 12 | + | "bnema.dev/ermite/internal/domain" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // LicenseBadge names the root license file and its display identifier. | |
| 16 | + | type LicenseBadge struct { | |
| 17 | + | ID, Href string | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | var licenseNames = []string{"license", "license.md", "license.txt", "licence", "licence.md", "licence.txt", "copying", "copying.md", "copying.txt"} | |
| 21 | + | ||
| 22 | + | // repositoryLicense caches both absence and detection by immutable head. The | |
| 23 | + | // caller has already authorized repository browsing. Failures are not cached. | |
| 24 | + | // A separate instance of the bounded immutable-result cache coalesces misses. | |
| 25 | + | func (s *Server) repositoryLicense(ctx context.Context, repo domain.RepoName, head string) *LicenseBadge { | |
| 26 | + | key := repo.String() + "\x00" + head | |
| 27 | + | body, err := s.licenses.load(ctx, repo.String(), key, func() ([]byte, error) { | |
| 28 | + | badge, err := s.detectLicense(ctx, repo, head) | |
| 29 | + | if err != nil { | |
| 30 | + | return nil, err | |
| 31 | + | } | |
| 32 | + | return json.Marshal(badge) | |
| 33 | + | }) | |
| 34 | + | if err != nil { | |
| 35 | + | log := zerowrap.FromCtx(ctx) | |
| 36 | + | log.Warn().Err(err).Str("repo", repo.String()).Msg("read the repository license") | |
| 37 | + | return nil | |
| 38 | + | } | |
| 39 | + | var badge *LicenseBadge | |
| 40 | + | if err := json.Unmarshal(body, &badge); err != nil { | |
| 41 | + | return nil | |
| 42 | + | } | |
| 43 | + | return badge | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | //hexcheck:ignore suspicious-business-logic-in-adapter picks a root license file for display only | |
| 47 | + | func (s *Server) detectLicense(ctx context.Context, repo domain.RepoName, head string) (*LicenseBadge, error) { | |
| 48 | + | tree, err := s.getTree(ctx, repo, head, "", false) | |
| 49 | + | if err != nil { | |
| 50 | + | return nil, err | |
| 51 | + | } | |
| 52 | + | for _, name := range licenseNames { | |
| 53 | + | for _, entry := range tree.Entries { | |
| 54 | + | if (entry.Kind != domain.EntryFile && entry.Kind != domain.EntryExecutable) || !strings.EqualFold(name, entry.Name) { | |
| 55 | + | continue | |
| 56 | + | } | |
| 57 | + | body, binary, err := s.readBlob(ctx, repo, head, entry.Path, 64<<10) | |
| 58 | + | if err != nil { | |
| 59 | + | return nil, err | |
| 60 | + | } | |
| 61 | + | id := "Other" | |
| 62 | + | if !binary { | |
| 63 | + | id = licenseID(string(body)) | |
| 64 | + | } | |
| 65 | + | return &LicenseBadge{ID: id, Href: repoHref(repo.String(), "blob", head, entry.Path)}, nil | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | return nil, nil | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // licenseID deliberately recognizes titles and characteristic grants, not | |
| 72 | + | // incidental mentions of other licenses in their full text. | |
| 73 | + | func licenseID(body string) string { | |
| 74 | + | for _, line := range strings.Split(body, "\n") { | |
| 75 | + | if i := strings.Index(strings.ToLower(line), "spdx-license-identifier:"); i >= 0 { | |
| 76 | + | id := strings.TrimSpace(line[i+len("spdx-license-identifier:"):]) | |
| 77 | + | id = strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(id, "-->"), "*/")) | |
| 78 | + | if id != "" && len(id) <= 160 { | |
| 79 | + | return id | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | text := strings.ToLower(strings.Join(strings.Fields(body), " ")) | |
| 84 | + | title := text[:min(len(text), 600)] | |
| 85 | + | switch { | |
| 86 | + | case strings.Contains(title, "gnu affero general public license") && strings.Contains(title, "version 3"): | |
| 87 | + | return "AGPL-3.0" | |
| 88 | + | case strings.Contains(title, "gnu lesser general public license") && strings.Contains(title, "version 3"): | |
| 89 | + | return "LGPL-3.0" | |
| 90 | + | case strings.Contains(title, "gnu lesser general public license") && strings.Contains(title, "version 2.1"): | |
| 91 | + | return "LGPL-2.1" | |
| 92 | + | case strings.Contains(title, "gnu general public license") && strings.Contains(title, "version 3"): | |
| 93 | + | return "GPL-3.0" | |
| 94 | + | case strings.Contains(title, "gnu general public license") && strings.Contains(title, "version 2"): | |
| 95 | + | return "GPL-2.0" | |
| 96 | + | case strings.Contains(title, "apache license") && strings.Contains(title, "version 2.0"): | |
| 97 | + | return "Apache-2.0" | |
| 98 | + | case strings.Contains(title, "mozilla public license") && strings.Contains(title, "2.0"): | |
| 99 | + | return "MPL-2.0" | |
| 100 | + | case strings.Contains(text, "permission is hereby granted, free of charge") && strings.Contains(text, "the software is provided \"as is\""): | |
| 101 | + | if strings.Contains(text, "the above copyright notice and this permission notice shall be included in all copies or substantial portions of the software") { | |
| 102 | + | return "MIT" | |
| 103 | + | } | |
| 104 | + | return "MIT-0" | |
| 105 | + | case strings.Contains(text, "all advertising materials"): | |
| 106 | + | return "Other" | |
| 107 | + | case strings.Contains(text, "redistribution and use in source and binary forms") && strings.Contains(text, "neither the name"): | |
| 108 | + | return "BSD-3-Clause" | |
| 109 | + | case strings.Contains(text, "redistribution and use in source and binary forms") && strings.Contains(text, "this list of conditions"): | |
| 110 | + | return "BSD-2-Clause" | |
| 111 | + | case strings.Contains(text, "permission to use, copy, modify, and/or distribute this software for any purpose with or without fee"): | |
| 112 | + | return "ISC" | |
| 113 | + | } | |
| 114 | + | return "Other" | |
| 115 | + | } | |
| @@ -0,0 +1,115 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package web | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "encoding/json" | |||
| 8 | + | "strings" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/zerowrap" | |||
| 11 | + | ||||
| 12 | + | "bnema.dev/ermite/internal/domain" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // LicenseBadge names the root license file and its display identifier. | |||
| 16 | + | type LicenseBadge struct { | |||
| 17 | + | ID, Href string | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | var licenseNames = []string{"license", "license.md", "license.txt", "licence", "licence.md", "licence.txt", "copying", "copying.md", "copying.txt"} | |||
| 21 | + | ||||
| 22 | + | // repositoryLicense caches both absence and detection by immutable head. The | |||
| 23 | + | // caller has already authorized repository browsing. Failures are not cached. | |||
| 24 | + | // A separate instance of the bounded immutable-result cache coalesces misses. | |||
| 25 | + | func (s *Server) repositoryLicense(ctx context.Context, repo domain.RepoName, head string) *LicenseBadge { | |||
| 26 | + | key := repo.String() + "\x00" + head | |||
| 27 | + | body, err := s.licenses.load(ctx, repo.String(), key, func() ([]byte, error) { | |||
| 28 | + | badge, err := s.detectLicense(ctx, repo, head) | |||
| 29 | + | if err != nil { | |||
| 30 | + | return nil, err | |||
| 31 | + | } | |||
| 32 | + | return json.Marshal(badge) | |||
| 33 | + | }) | |||
| 34 | + | if err != nil { | |||
| 35 | + | log := zerowrap.FromCtx(ctx) | |||
| 36 | + | log.Warn().Err(err).Str("repo", repo.String()).Msg("read the repository license") | |||
| 37 | + | return nil | |||
| 38 | + | } | |||
| 39 | + | var badge *LicenseBadge | |||
| 40 | + | if err := json.Unmarshal(body, &badge); err != nil { | |||
| 41 | + | return nil | |||
| 42 | + | } | |||
| 43 | + | return badge | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | //hexcheck:ignore suspicious-business-logic-in-adapter picks a root license file for display only | |||
| 47 | + | func (s *Server) detectLicense(ctx context.Context, repo domain.RepoName, head string) (*LicenseBadge, error) { | |||
| 48 | + | tree, err := s.getTree(ctx, repo, head, "", false) | |||
| 49 | + | if err != nil { | |||
| 50 | + | return nil, err | |||
| 51 | + | } | |||
| 52 | + | for _, name := range licenseNames { | |||
| 53 | + | for _, entry := range tree.Entries { | |||
| 54 | + | if (entry.Kind != domain.EntryFile && entry.Kind != domain.EntryExecutable) || !strings.EqualFold(name, entry.Name) { | |||
| 55 | + | continue | |||
| 56 | + | } | |||
| 57 | + | body, binary, err := s.readBlob(ctx, repo, head, entry.Path, 64<<10) | |||
| 58 | + | if err != nil { | |||
| 59 | + | return nil, err | |||
| 60 | + | } | |||
| 61 | + | id := "Other" | |||
| 62 | + | if !binary { | |||
| 63 | + | id = licenseID(string(body)) | |||
| 64 | + | } | |||
| 65 | + | return &LicenseBadge{ID: id, Href: repoHref(repo.String(), "blob", head, entry.Path)}, nil | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | return nil, nil | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // licenseID deliberately recognizes titles and characteristic grants, not | |||
| 72 | + | // incidental mentions of other licenses in their full text. | |||
| 73 | + | func licenseID(body string) string { | |||
| 74 | + | for _, line := range strings.Split(body, "\n") { | |||
| 75 | + | if i := strings.Index(strings.ToLower(line), "spdx-license-identifier:"); i >= 0 { | |||
| 76 | + | id := strings.TrimSpace(line[i+len("spdx-license-identifier:"):]) | |||
| 77 | + | id = strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(id, "-->"), "*/")) | |||
| 78 | + | if id != "" && len(id) <= 160 { | |||
| 79 | + | return id | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | text := strings.ToLower(strings.Join(strings.Fields(body), " ")) | |||
| 84 | + | title := text[:min(len(text), 600)] | |||
| 85 | + | switch { | |||
| 86 | + | case strings.Contains(title, "gnu affero general public license") && strings.Contains(title, "version 3"): | |||
| 87 | + | return "AGPL-3.0" | |||
| 88 | + | case strings.Contains(title, "gnu lesser general public license") && strings.Contains(title, "version 3"): | |||
| 89 | + | return "LGPL-3.0" | |||
| 90 | + | case strings.Contains(title, "gnu lesser general public license") && strings.Contains(title, "version 2.1"): | |||
| 91 | + | return "LGPL-2.1" | |||
| 92 | + | case strings.Contains(title, "gnu general public license") && strings.Contains(title, "version 3"): | |||
| 93 | + | return "GPL-3.0" | |||
| 94 | + | case strings.Contains(title, "gnu general public license") && strings.Contains(title, "version 2"): | |||
| 95 | + | return "GPL-2.0" | |||
| 96 | + | case strings.Contains(title, "apache license") && strings.Contains(title, "version 2.0"): | |||
| 97 | + | return "Apache-2.0" | |||
| 98 | + | case strings.Contains(title, "mozilla public license") && strings.Contains(title, "2.0"): | |||
| 99 | + | return "MPL-2.0" | |||
| 100 | + | case strings.Contains(text, "permission is hereby granted, free of charge") && strings.Contains(text, "the software is provided \"as is\""): | |||
| 101 | + | if strings.Contains(text, "the above copyright notice and this permission notice shall be included in all copies or substantial portions of the software") { | |||
| 102 | + | return "MIT" | |||
| 103 | + | } | |||
| 104 | + | return "MIT-0" | |||
| 105 | + | case strings.Contains(text, "all advertising materials"): | |||
| 106 | + | return "Other" | |||
| 107 | + | case strings.Contains(text, "redistribution and use in source and binary forms") && strings.Contains(text, "neither the name"): | |||
| 108 | + | return "BSD-3-Clause" | |||
| 109 | + | case strings.Contains(text, "redistribution and use in source and binary forms") && strings.Contains(text, "this list of conditions"): | |||
| 110 | + | return "BSD-2-Clause" | |||
| 111 | + | case strings.Contains(text, "permission to use, copy, modify, and/or distribute this software for any purpose with or without fee"): | |||
| 112 | + | return "ISC" | |||
| 113 | + | } | |||
| 114 | + | return "Other" | |||
| 115 | + | } | |||
A internal/adapters/web/license_test.go +188 -0
| @@ -0,0 +1,188 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "errors" | |
| 8 | + | "io" | |
| 9 | + | "strings" | |
| 10 | + | "testing" | |
| 11 | + | ||
| 12 | + | "github.com/stretchr/testify/assert" | |
| 13 | + | "github.com/stretchr/testify/mock" | |
| 14 | + | "github.com/stretchr/testify/require" | |
| 15 | + | ||
| 16 | + | "bnema.dev/ermite/internal/domain" | |
| 17 | + | "bnema.dev/ermite/internal/ports" | |
| 18 | + | "bnema.dev/ermite/internal/ports/mocks" | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | func TestLicenseID(t *testing.T) { | |
| 22 | + | t.Parallel() | |
| 23 | + | for _, tc := range []struct{ text, want string }{ | |
| 24 | + | {"// SPDX-License-Identifier: MIT", "MIT"}, | |
| 25 | + | {"<!-- SPDX-License-Identifier: Apache-2.0 OR MIT -->", "Apache-2.0 OR MIT"}, | |
| 26 | + | {"/* SPDX-License-Identifier: GPL-3.0-or-later */", "GPL-3.0-or-later"}, | |
| 27 | + | {"GNU AFFERO GENERAL PUBLIC LICENSE\nVersion 3, 19 November 2007", "AGPL-3.0"}, | |
| 28 | + | {"GNU GENERAL PUBLIC LICENSE Version 3" + strings.Repeat(" padding", 100) + "GNU Affero General Public License version 3", "GPL-3.0"}, | |
| 29 | + | {"GNU GENERAL PUBLIC LICENSE Version 2", "GPL-2.0"}, | |
| 30 | + | {"GNU LESSER GENERAL PUBLIC LICENSE Version 2.1", "LGPL-2.1"}, | |
| 31 | + | {"GNU LESSER GENERAL PUBLIC LICENSE Version 3", "LGPL-3.0"}, | |
| 32 | + | {"Apache License\nVersion 2.0, January 2004", "Apache-2.0"}, | |
| 33 | + | {"Mozilla Public License Version 2.0", "MPL-2.0"}, | |
| 34 | + | {"Permission is hereby granted, free of charge\nTHE SOFTWARE IS PROVIDED \"AS IS\"", "MIT-0"}, | |
| 35 | + | {"Redistribution and use in source and binary forms ... neither the name", "BSD-3-Clause"}, | |
| 36 | + | {"Redistribution and use in source and binary forms ... this list of conditions", "BSD-2-Clause"}, | |
| 37 | + | {"Redistribution and use in source and binary forms ... all advertising materials ... neither the name", "Other"}, | |
| 38 | + | {"Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee", "ISC"}, | |
| 39 | + | {"Custom license mentioning MIT or Apache-2.0", "Other"}, | |
| 40 | + | {"", "Other"}, | |
| 41 | + | } { | |
| 42 | + | t.Run(tc.want+tc.text[:min(20, len(tc.text))], func(t *testing.T) { t.Parallel(); assert.Equal(t, tc.want, licenseID(tc.text)) }) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func TestRepositoryLicenseCache(t *testing.T) { | |
| 47 | + | t.Parallel() | |
| 48 | + | for _, filename := range append(append([]string{}, licenseNames...), "LiCeNsE.MD", "missing", "directory") { | |
| 49 | + | t.Run(filename, func(t *testing.T) { | |
| 50 | + | t.Parallel() | |
| 51 | + | browse := mocks.NewBrowseService(t) | |
| 52 | + | repo := repoName(t, "proj") | |
| 53 | + | head := strings.Repeat("a", 40) | |
| 54 | + | entry := domain.TreeEntry{Name: filename, Path: filename, Kind: domain.EntryFile} | |
| 55 | + | if filename == "directory" { | |
| 56 | + | entry.Name, entry.Kind = "LICENSE", domain.EntryDirectory | |
| 57 | + | } | |
| 58 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{Entries: []domain.TreeEntry{entry}}, nil).Once() | |
| 59 | + | found := filename != "missing" && filename != "directory" | |
| 60 | + | if found { | |
| 61 | + | browse.EXPECT().OpenBlob(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything).Return(domain.BlobInfo{}, io.NopCloser(strings.NewReader("SPDX-License-Identifier: MIT")), nil).Once() | |
| 62 | + | } | |
| 63 | + | srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: browse}) | |
| 64 | + | for range 2 { | |
| 65 | + | badge := srv.repositoryLicense(t.Context(), repo, head) | |
| 66 | + | if !found { | |
| 67 | + | assert.Nil(t, badge) | |
| 68 | + | continue | |
| 69 | + | } | |
| 70 | + | require.NotNil(t, badge) | |
| 71 | + | assert.Equal(t, "MIT", badge.ID) | |
| 72 | + | assert.Equal(t, "/proj/blob/"+head+"/"+filename, badge.Href) | |
| 73 | + | } | |
| 74 | + | }) | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func TestRepositoryLicenseRetriesErrorsAndNewHeads(t *testing.T) { | |
| 79 | + | t.Parallel() | |
| 80 | + | browse := mocks.NewBrowseService(t) | |
| 81 | + | repo := repoName(t, "proj") | |
| 82 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{}, errors.New("unavailable")).Once() | |
| 83 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{}, nil).Twice() | |
| 84 | + | srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: browse}) | |
| 85 | + | for _, head := range []string{strings.Repeat("a", 40), strings.Repeat("a", 40), strings.Repeat("b", 40)} { | |
| 86 | + | assert.Nil(t, srv.repositoryLicense(context.Background(), repo, head)) | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | func TestSummaryLicense(t *testing.T) { | |
| 91 | + | t.Parallel() | |
| 92 | + | assert.NotContains(t, renderRail(t, SummaryPage{}), "<dt>license</dt>") | |
| 93 | + | out := renderRail(t, SummaryPage{License: &LicenseBadge{ID: "MIT", Href: "/proj/blob/main/LICENSE"}}) | |
| 94 | + | assert.Contains(t, out, `<dt>license</dt><dd><a href="/proj/blob/main/LICENSE">MIT</a></dd>`) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | func TestHomeLicenseFromDefaultHead(t *testing.T) { | |
| 98 | + | t.Parallel() | |
| 99 | + | for _, binary := range []bool{false, true} { | |
| 100 | + | name := "unknown text" | |
| 101 | + | if binary { | |
| 102 | + | name = "binary" | |
| 103 | + | } | |
| 104 | + | t.Run(name, func(t *testing.T) { | |
| 105 | + | t.Parallel() | |
| 106 | + | browse := mocks.NewBrowseService(t) | |
| 107 | + | repo := pushedRepo(t, "proj") | |
| 108 | + | commits := homeCommits(t, 1) | |
| 109 | + | head := commits[0].ID.String() | |
| 110 | + | revision, err := domain.ParseRevision(head) | |
| 111 | + | require.NoError(t, err) | |
| 112 | + | browse.EXPECT().ListCommits(mock.Anything, mock.Anything, repo.Name, mock.Anything).Return(ports.HistoryPage{Commits: commits}, nil).Once() | |
| 113 | + | browse.EXPECT().RepoStats(mock.Anything, mock.Anything, repo.Name, commits[0].ID, mock.Anything).Return(ports.RepoStats{}, nil).Once() | |
| 114 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo.Name, revision, mock.Anything, false).Return(domain.Tree{Entries: []domain.TreeEntry{{Name: "COPYING", Path: "COPYING", Kind: domain.EntryFile}}}, nil).Once() | |
| 115 | + | browse.EXPECT().OpenBlob(mock.Anything, mock.Anything, repo.Name, revision, mock.Anything).Return(domain.BlobInfo{Binary: binary}, io.NopCloser(strings.NewReader("A custom license")), nil).Once() | |
| 116 | + | srv := newTestServer(t, Config{Repos: reposServing(t, repo), Browse: browse}) | |
| 117 | + | row := srv.homeRowOf(t.Context(), repo, fixedNow, false) | |
| 118 | + | require.NotNil(t, row.License) | |
| 119 | + | assert.Equal(t, "Other", row.License.ID) | |
| 120 | + | assert.Contains(t, renderRow(t, row), `href="/proj/blob/`+head+`/COPYING" title="License: Other">Other</a>`) | |
| 121 | + | assert.Contains(t, renderRail(t, SummaryPage{License: row.License}), `<dt>license</dt><dd><a href="/proj/blob/`+head+`/COPYING">Other</a></dd>`) | |
| 122 | + | }) | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | func TestLicenseFullTexts(t *testing.T) { | |
| 127 | + | t.Parallel() | |
| 128 | + | const grant = `Copyright (c) 2026 Example | |
| 129 | + | ||
| 130 | + | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 131 | + | of this software and associated documentation files (the "Software"), to deal | |
| 132 | + | in the Software without restriction, including without limitation the rights | |
| 133 | + | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 134 | + | copies of the Software, and to permit persons to whom the Software is | |
| 135 | + | furnished to do so.` | |
| 136 | + | const notice = `The above copyright notice and this permission notice shall be included in | |
| 137 | + | all copies or substantial portions of the Software.` | |
| 138 | + | const warranty = `THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 139 | + | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 140 | + | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 141 | + | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 142 | + | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 143 | + | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 144 | + | THE SOFTWARE.` | |
| 145 | + | const bsd = `Copyright (c) 2026 Example. All rights reserved. | |
| 146 | + | Redistribution and use in source and binary forms, with or without | |
| 147 | + | modification, are permitted provided that the following conditions are met: | |
| 148 | + | 1. Redistributions of source code must retain the above copyright notice, | |
| 149 | + | this list of conditions and the following disclaimer. | |
| 150 | + | 2. Redistributions in binary form must reproduce the above copyright notice, | |
| 151 | + | this list of conditions and the following disclaimer in the documentation | |
| 152 | + | and/or other materials provided with the distribution.` | |
| 153 | + | const endorsement = `3. Neither the name of the copyright holder nor the names of its | |
| 154 | + | contributors may be used to endorse or promote products derived from | |
| 155 | + | this software without specific prior written permission.` | |
| 156 | + | const disclaimer = `THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| 157 | + | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| 158 | + | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
| 159 | + | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |
| 160 | + | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| 161 | + | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| 162 | + | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| 163 | + | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| 164 | + | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| 165 | + | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| 166 | + | POSSIBILITY OF SUCH DAMAGE.` | |
| 167 | + | const gnu = `Copyright (C) 2007 Free Software Foundation, Inc. | |
| 168 | + | Everyone is permitted to copy and distribute verbatim copies of this license | |
| 169 | + | document, but changing it is not allowed. | |
| 170 | + | ||
| 171 | + | Preamble | |
| 172 | + | The licenses for most software and other practical works are designed to | |
| 173 | + | take away your freedom to share and change the works. By contrast, the GNU | |
| 174 | + | General Public License is intended to guarantee your freedom to share and | |
| 175 | + | change all versions of a program--to make sure it remains free software | |
| 176 | + | for all its users.` | |
| 177 | + | for _, tc := range []struct{ name, text, want string }{ | |
| 178 | + | {"MIT", "MIT License\n" + grant + "\n" + notice + "\n" + warranty, "MIT"}, | |
| 179 | + | {"MIT-0", "MIT No Attribution\n" + grant + "\n" + warranty, "MIT-0"}, | |
| 180 | + | {"BSD-2", bsd + "\n" + disclaimer, "BSD-2-Clause"}, | |
| 181 | + | {"BSD-3", bsd + "\n" + endorsement + "\n" + disclaimer, "BSD-3-Clause"}, | |
| 182 | + | {"GPL", "GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\n" + gnu + strings.Repeat("\nTerms and conditions for copying, distribution and modification.", 10) + "\nUse with the GNU Affero General Public License version 3.", "GPL-3.0"}, | |
| 183 | + | {"LGPL", "GNU LESSER GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\n" + gnu + "\nThis version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by additional permissions.", "LGPL-3.0"}, | |
| 184 | + | {"AGPL", "GNU AFFERO GENERAL PUBLIC LICENSE\nVersion 3, 19 November 2007\n" + gnu + "\nNotwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network an opportunity to receive the Corresponding Source.", "AGPL-3.0"}, | |
| 185 | + | } { | |
| 186 | + | t.Run(tc.name, func(t *testing.T) { t.Parallel(); assert.Equal(t, tc.want, licenseID(tc.text)) }) | |
| 187 | + | } | |
| 188 | + | } | |
| @@ -0,0 +1,188 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package web | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "errors" | |||
| 8 | + | "io" | |||
| 9 | + | "strings" | |||
| 10 | + | "testing" | |||
| 11 | + | ||||
| 12 | + | "github.com/stretchr/testify/assert" | |||
| 13 | + | "github.com/stretchr/testify/mock" | |||
| 14 | + | "github.com/stretchr/testify/require" | |||
| 15 | + | ||||
| 16 | + | "bnema.dev/ermite/internal/domain" | |||
| 17 | + | "bnema.dev/ermite/internal/ports" | |||
| 18 | + | "bnema.dev/ermite/internal/ports/mocks" | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | func TestLicenseID(t *testing.T) { | |||
| 22 | + | t.Parallel() | |||
| 23 | + | for _, tc := range []struct{ text, want string }{ | |||
| 24 | + | {"// SPDX-License-Identifier: MIT", "MIT"}, | |||
| 25 | + | {"<!-- SPDX-License-Identifier: Apache-2.0 OR MIT -->", "Apache-2.0 OR MIT"}, | |||
| 26 | + | {"/* SPDX-License-Identifier: GPL-3.0-or-later */", "GPL-3.0-or-later"}, | |||
| 27 | + | {"GNU AFFERO GENERAL PUBLIC LICENSE\nVersion 3, 19 November 2007", "AGPL-3.0"}, | |||
| 28 | + | {"GNU GENERAL PUBLIC LICENSE Version 3" + strings.Repeat(" padding", 100) + "GNU Affero General Public License version 3", "GPL-3.0"}, | |||
| 29 | + | {"GNU GENERAL PUBLIC LICENSE Version 2", "GPL-2.0"}, | |||
| 30 | + | {"GNU LESSER GENERAL PUBLIC LICENSE Version 2.1", "LGPL-2.1"}, | |||
| 31 | + | {"GNU LESSER GENERAL PUBLIC LICENSE Version 3", "LGPL-3.0"}, | |||
| 32 | + | {"Apache License\nVersion 2.0, January 2004", "Apache-2.0"}, | |||
| 33 | + | {"Mozilla Public License Version 2.0", "MPL-2.0"}, | |||
| 34 | + | {"Permission is hereby granted, free of charge\nTHE SOFTWARE IS PROVIDED \"AS IS\"", "MIT-0"}, | |||
| 35 | + | {"Redistribution and use in source and binary forms ... neither the name", "BSD-3-Clause"}, | |||
| 36 | + | {"Redistribution and use in source and binary forms ... this list of conditions", "BSD-2-Clause"}, | |||
| 37 | + | {"Redistribution and use in source and binary forms ... all advertising materials ... neither the name", "Other"}, | |||
| 38 | + | {"Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee", "ISC"}, | |||
| 39 | + | {"Custom license mentioning MIT or Apache-2.0", "Other"}, | |||
| 40 | + | {"", "Other"}, | |||
| 41 | + | } { | |||
| 42 | + | t.Run(tc.want+tc.text[:min(20, len(tc.text))], func(t *testing.T) { t.Parallel(); assert.Equal(t, tc.want, licenseID(tc.text)) }) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func TestRepositoryLicenseCache(t *testing.T) { | |||
| 47 | + | t.Parallel() | |||
| 48 | + | for _, filename := range append(append([]string{}, licenseNames...), "LiCeNsE.MD", "missing", "directory") { | |||
| 49 | + | t.Run(filename, func(t *testing.T) { | |||
| 50 | + | t.Parallel() | |||
| 51 | + | browse := mocks.NewBrowseService(t) | |||
| 52 | + | repo := repoName(t, "proj") | |||
| 53 | + | head := strings.Repeat("a", 40) | |||
| 54 | + | entry := domain.TreeEntry{Name: filename, Path: filename, Kind: domain.EntryFile} | |||
| 55 | + | if filename == "directory" { | |||
| 56 | + | entry.Name, entry.Kind = "LICENSE", domain.EntryDirectory | |||
| 57 | + | } | |||
| 58 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{Entries: []domain.TreeEntry{entry}}, nil).Once() | |||
| 59 | + | found := filename != "missing" && filename != "directory" | |||
| 60 | + | if found { | |||
| 61 | + | browse.EXPECT().OpenBlob(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything).Return(domain.BlobInfo{}, io.NopCloser(strings.NewReader("SPDX-License-Identifier: MIT")), nil).Once() | |||
| 62 | + | } | |||
| 63 | + | srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: browse}) | |||
| 64 | + | for range 2 { | |||
| 65 | + | badge := srv.repositoryLicense(t.Context(), repo, head) | |||
| 66 | + | if !found { | |||
| 67 | + | assert.Nil(t, badge) | |||
| 68 | + | continue | |||
| 69 | + | } | |||
| 70 | + | require.NotNil(t, badge) | |||
| 71 | + | assert.Equal(t, "MIT", badge.ID) | |||
| 72 | + | assert.Equal(t, "/proj/blob/"+head+"/"+filename, badge.Href) | |||
| 73 | + | } | |||
| 74 | + | }) | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func TestRepositoryLicenseRetriesErrorsAndNewHeads(t *testing.T) { | |||
| 79 | + | t.Parallel() | |||
| 80 | + | browse := mocks.NewBrowseService(t) | |||
| 81 | + | repo := repoName(t, "proj") | |||
| 82 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{}, errors.New("unavailable")).Once() | |||
| 83 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{}, nil).Twice() | |||
| 84 | + | srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: browse}) | |||
| 85 | + | for _, head := range []string{strings.Repeat("a", 40), strings.Repeat("a", 40), strings.Repeat("b", 40)} { | |||
| 86 | + | assert.Nil(t, srv.repositoryLicense(context.Background(), repo, head)) | |||
| 87 | + | } | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | func TestSummaryLicense(t *testing.T) { | |||
| 91 | + | t.Parallel() | |||
| 92 | + | assert.NotContains(t, renderRail(t, SummaryPage{}), "<dt>license</dt>") | |||
| 93 | + | out := renderRail(t, SummaryPage{License: &LicenseBadge{ID: "MIT", Href: "/proj/blob/main/LICENSE"}}) | |||
| 94 | + | assert.Contains(t, out, `<dt>license</dt><dd><a href="/proj/blob/main/LICENSE">MIT</a></dd>`) | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | func TestHomeLicenseFromDefaultHead(t *testing.T) { | |||
| 98 | + | t.Parallel() | |||
| 99 | + | for _, binary := range []bool{false, true} { | |||
| 100 | + | name := "unknown text" | |||
| 101 | + | if binary { | |||
| 102 | + | name = "binary" | |||
| 103 | + | } | |||
| 104 | + | t.Run(name, func(t *testing.T) { | |||
| 105 | + | t.Parallel() | |||
| 106 | + | browse := mocks.NewBrowseService(t) | |||
| 107 | + | repo := pushedRepo(t, "proj") | |||
| 108 | + | commits := homeCommits(t, 1) | |||
| 109 | + | head := commits[0].ID.String() | |||
| 110 | + | revision, err := domain.ParseRevision(head) | |||
| 111 | + | require.NoError(t, err) | |||
| 112 | + | browse.EXPECT().ListCommits(mock.Anything, mock.Anything, repo.Name, mock.Anything).Return(ports.HistoryPage{Commits: commits}, nil).Once() | |||
| 113 | + | browse.EXPECT().RepoStats(mock.Anything, mock.Anything, repo.Name, commits[0].ID, mock.Anything).Return(ports.RepoStats{}, nil).Once() | |||
| 114 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo.Name, revision, mock.Anything, false).Return(domain.Tree{Entries: []domain.TreeEntry{{Name: "COPYING", Path: "COPYING", Kind: domain.EntryFile}}}, nil).Once() | |||
| 115 | + | browse.EXPECT().OpenBlob(mock.Anything, mock.Anything, repo.Name, revision, mock.Anything).Return(domain.BlobInfo{Binary: binary}, io.NopCloser(strings.NewReader("A custom license")), nil).Once() | |||
| 116 | + | srv := newTestServer(t, Config{Repos: reposServing(t, repo), Browse: browse}) | |||
| 117 | + | row := srv.homeRowOf(t.Context(), repo, fixedNow, false) | |||
| 118 | + | require.NotNil(t, row.License) | |||
| 119 | + | assert.Equal(t, "Other", row.License.ID) | |||
| 120 | + | assert.Contains(t, renderRow(t, row), `href="/proj/blob/`+head+`/COPYING" title="License: Other">Other</a>`) | |||
| 121 | + | assert.Contains(t, renderRail(t, SummaryPage{License: row.License}), `<dt>license</dt><dd><a href="/proj/blob/`+head+`/COPYING">Other</a></dd>`) | |||
| 122 | + | }) | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | func TestLicenseFullTexts(t *testing.T) { | |||
| 127 | + | t.Parallel() | |||
| 128 | + | const grant = `Copyright (c) 2026 Example | |||
| 129 | + | ||||
| 130 | + | Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| 131 | + | of this software and associated documentation files (the "Software"), to deal | |||
| 132 | + | in the Software without restriction, including without limitation the rights | |||
| 133 | + | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| 134 | + | copies of the Software, and to permit persons to whom the Software is | |||
| 135 | + | furnished to do so.` | |||
| 136 | + | const notice = `The above copyright notice and this permission notice shall be included in | |||
| 137 | + | all copies or substantial portions of the Software.` | |||
| 138 | + | const warranty = `THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| 139 | + | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| 140 | + | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| 141 | + | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| 142 | + | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| 143 | + | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
| 144 | + | THE SOFTWARE.` | |||
| 145 | + | const bsd = `Copyright (c) 2026 Example. All rights reserved. | |||
| 146 | + | Redistribution and use in source and binary forms, with or without | |||
| 147 | + | modification, are permitted provided that the following conditions are met: | |||
| 148 | + | 1. Redistributions of source code must retain the above copyright notice, | |||
| 149 | + | this list of conditions and the following disclaimer. | |||
| 150 | + | 2. Redistributions in binary form must reproduce the above copyright notice, | |||
| 151 | + | this list of conditions and the following disclaimer in the documentation | |||
| 152 | + | and/or other materials provided with the distribution.` | |||
| 153 | + | const endorsement = `3. Neither the name of the copyright holder nor the names of its | |||
| 154 | + | contributors may be used to endorse or promote products derived from | |||
| 155 | + | this software without specific prior written permission.` | |||
| 156 | + | const disclaimer = `THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
| 157 | + | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
| 158 | + | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||
| 159 | + | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |||
| 160 | + | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |||
| 161 | + | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |||
| 162 | + | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |||
| 163 | + | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |||
| 164 | + | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |||
| 165 | + | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |||
| 166 | + | POSSIBILITY OF SUCH DAMAGE.` | |||
| 167 | + | const gnu = `Copyright (C) 2007 Free Software Foundation, Inc. | |||
| 168 | + | Everyone is permitted to copy and distribute verbatim copies of this license | |||
| 169 | + | document, but changing it is not allowed. | |||
| 170 | + | ||||
| 171 | + | Preamble | |||
| 172 | + | The licenses for most software and other practical works are designed to | |||
| 173 | + | take away your freedom to share and change the works. By contrast, the GNU | |||
| 174 | + | General Public License is intended to guarantee your freedom to share and | |||
| 175 | + | change all versions of a program--to make sure it remains free software | |||
| 176 | + | for all its users.` | |||
| 177 | + | for _, tc := range []struct{ name, text, want string }{ | |||
| 178 | + | {"MIT", "MIT License\n" + grant + "\n" + notice + "\n" + warranty, "MIT"}, | |||
| 179 | + | {"MIT-0", "MIT No Attribution\n" + grant + "\n" + warranty, "MIT-0"}, | |||
| 180 | + | {"BSD-2", bsd + "\n" + disclaimer, "BSD-2-Clause"}, | |||
| 181 | + | {"BSD-3", bsd + "\n" + endorsement + "\n" + disclaimer, "BSD-3-Clause"}, | |||
| 182 | + | {"GPL", "GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\n" + gnu + strings.Repeat("\nTerms and conditions for copying, distribution and modification.", 10) + "\nUse with the GNU Affero General Public License version 3.", "GPL-3.0"}, | |||
| 183 | + | {"LGPL", "GNU LESSER GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\n" + gnu + "\nThis version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by additional permissions.", "LGPL-3.0"}, | |||
| 184 | + | {"AGPL", "GNU AFFERO GENERAL PUBLIC LICENSE\nVersion 3, 19 November 2007\n" + gnu + "\nNotwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network an opportunity to receive the Corresponding Source.", "AGPL-3.0"}, | |||
| 185 | + | } { | |||
| 186 | + | t.Run(tc.name, func(t *testing.T) { t.Parallel(); assert.Equal(t, tc.want, licenseID(tc.text)) }) | |||
| 187 | + | } | |||
| 188 | + | } | |||
M internal/adapters/web/pages.templ +5 -1
| @@ -69,7 +69,7 @@ templ Home(l Layout, p HomePage) { | |||
| 69 | 69 | templ Summary(l Layout, p SummaryPage) { | |
| 70 | 70 | @Shell(l, summaryRail(p)) { | |
| 71 | 71 | if p.Readme != nil { | |
| 72 | - | @ReadmeCard(*p.Readme) | |
| 72 | + | @ReadmeCard(*p.Readme, p.Docs...) | |
| 73 | 73 | } | |
| 74 | 74 | } | |
| 75 | 75 | } | |
| @@ -112,6 +112,10 @@ templ summaryRail(p SummaryPage) { | |||
| 112 | 112 | <dt>{ f.Key }</dt> | |
| 113 | 113 | @FactValue(f) | |
| 114 | 114 | } | |
| 115 | + | if lic := p.License; lic != nil { | |
| 116 | + | <dt>license</dt> | |
| 117 | + | <dd><a href={ templ.SafeURL(lic.Href) }>{ lic.ID }</a></dd> | |
| 118 | + | } | |
| 115 | 119 | if p.Latest != nil { | |
| 116 | 120 | @latestTagFact(*p.Latest) | |
| 117 | 121 | } | |
| @@ -69,7 +69,7 @@ templ Home(l Layout, p HomePage) { | |||||
| 69 | templ Summary(l Layout, p SummaryPage) { | 69 | templ Summary(l Layout, p SummaryPage) { | ||
| 70 | @Shell(l, summaryRail(p)) { | 70 | @Shell(l, summaryRail(p)) { | ||
| 71 | if p.Readme != nil { | 71 | if p.Readme != nil { | ||
| 72 | - | @ReadmeCard(*p.Readme) | 72 | + | @ReadmeCard(*p.Readme, p.Docs...) |
| 73 | } | 73 | } | ||
| 74 | } | 74 | } | ||
| 75 | } | 75 | } | ||
| @@ -112,6 +112,10 @@ templ summaryRail(p SummaryPage) { | |||||
| 112 | <dt>{ f.Key }</dt> | 112 | <dt>{ f.Key }</dt> | ||
| 113 | @FactValue(f) | 113 | @FactValue(f) | ||
| 114 | } | 114 | } | ||
| 115 | + | if lic := p.License; lic != nil { | |||
| 116 | + | <dt>license</dt> | |||
| 117 | + | <dd><a href={ templ.SafeURL(lic.Href) }>{ lic.ID }</a></dd> | |||
| 118 | + | } | |||
| 115 | if p.Latest != nil { | 119 | if p.Latest != nil { | ||
| 116 | @latestTagFact(*p.Latest) | 120 | @latestTagFact(*p.Latest) | ||
| 117 | } | 121 | } | ||
M internal/adapters/web/pages_templ.go +331 -299
| @@ -224,7 +224,7 @@ func Summary(l Layout, p SummaryPage) templ.Component { | |||
| 224 | 224 | } | |
| 225 | 225 | ctx = templ.InitializeContext(ctx) | |
| 226 | 226 | if p.Readme != nil { | |
| 227 | - | templ_7745c5c3_Err = ReadmeCard(*p.Readme).Render(ctx, templ_7745c5c3_Buffer) | |
| 227 | + | templ_7745c5c3_Err = ReadmeCard(*p.Readme, p.Docs...).Render(ctx, templ_7745c5c3_Buffer) | |
| 228 | 228 | if templ_7745c5c3_Err != nil { | |
| 229 | 229 | return templ_7745c5c3_Err | |
| 230 | 230 | } | |
| @@ -528,21 +528,15 @@ func summaryRail(p SummaryPage) templ.Component { | |||
| 528 | 528 | return templ_7745c5c3_Err | |
| 529 | 529 | } | |
| 530 | 530 | } | |
| 531 | - | if p.Latest != nil { | |
| 532 | - | templ_7745c5c3_Err = latestTagFact(*p.Latest).Render(ctx, templ_7745c5c3_Buffer) | |
| 533 | - | if templ_7745c5c3_Err != nil { | |
| 534 | - | return templ_7745c5c3_Err | |
| 535 | - | } | |
| 536 | - | } | |
| 537 | - | if r := p.Release; r != nil { | |
| 538 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<dt>release</dt><dd><a href=\"") | |
| 531 | + | if lic := p.License; lic != nil { | |
| 532 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<dt>license</dt><dd><a href=\"") | |
| 539 | 533 | if templ_7745c5c3_Err != nil { | |
| 540 | 534 | return templ_7745c5c3_Err | |
| 541 | 535 | } | |
| 542 | 536 | var templ_7745c5c3_Var27 templ.SafeURL | |
| 543 | - | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | |
| 537 | + | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(lic.Href)) | |
| 544 | 538 | if templ_7745c5c3_Err != nil { | |
| 545 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 36} | |
| 539 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 117, Col: 41} | |
| 546 | 540 | } | |
| 547 | 541 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | |
| 548 | 542 | if templ_7745c5c3_Err != nil { | |
| @@ -553,116 +547,154 @@ func summaryRail(p SummaryPage) templ.Component { | |||
| 553 | 547 | return templ_7745c5c3_Err | |
| 554 | 548 | } | |
| 555 | 549 | var templ_7745c5c3_Var28 string | |
| 556 | - | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(r.Tag) | |
| 550 | + | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(lic.ID) | |
| 557 | 551 | if templ_7745c5c3_Err != nil { | |
| 558 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 46} | |
| 552 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 117, Col: 52} | |
| 559 | 553 | } | |
| 560 | 554 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) | |
| 561 | 555 | if templ_7745c5c3_Err != nil { | |
| 562 | 556 | return templ_7745c5c3_Err | |
| 563 | 557 | } | |
| 564 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</a>") | |
| 558 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</a></dd>") | |
| 559 | + | if templ_7745c5c3_Err != nil { | |
| 560 | + | return templ_7745c5c3_Err | |
| 561 | + | } | |
| 562 | + | } | |
| 563 | + | if p.Latest != nil { | |
| 564 | + | templ_7745c5c3_Err = latestTagFact(*p.Latest).Render(ctx, templ_7745c5c3_Buffer) | |
| 565 | + | if templ_7745c5c3_Err != nil { | |
| 566 | + | return templ_7745c5c3_Err | |
| 567 | + | } | |
| 568 | + | } | |
| 569 | + | if r := p.Release; r != nil { | |
| 570 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<dt>release</dt><dd><a href=\"") | |
| 565 | 571 | if templ_7745c5c3_Err != nil { | |
| 566 | 572 | return templ_7745c5c3_Err | |
| 567 | 573 | } | |
| 568 | - | var templ_7745c5c3_Var29 string | |
| 569 | - | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 574 | + | var templ_7745c5c3_Var29 templ.SafeURL | |
| 575 | + | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | |
| 570 | 576 | if templ_7745c5c3_Err != nil { | |
| 571 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 60} | |
| 577 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 125, Col: 36} | |
| 572 | 578 | } | |
| 573 | 579 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | |
| 574 | 580 | if templ_7745c5c3_Err != nil { | |
| 575 | 581 | return templ_7745c5c3_Err | |
| 576 | 582 | } | |
| 577 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " <time datetime=\"") | |
| 583 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">") | |
| 578 | 584 | if templ_7745c5c3_Err != nil { | |
| 579 | 585 | return templ_7745c5c3_Err | |
| 580 | 586 | } | |
| 581 | 587 | var templ_7745c5c3_Var30 string | |
| 582 | - | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | |
| 588 | + | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(r.Tag) | |
| 583 | 589 | if templ_7745c5c3_Err != nil { | |
| 584 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 30} | |
| 590 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 125, Col: 46} | |
| 585 | 591 | } | |
| 586 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30) | |
| 592 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | |
| 587 | 593 | if templ_7745c5c3_Err != nil { | |
| 588 | 594 | return templ_7745c5c3_Err | |
| 589 | 595 | } | |
| 590 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" title=\"") | |
| 596 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</a>") | |
| 591 | 597 | if templ_7745c5c3_Err != nil { | |
| 592 | 598 | return templ_7745c5c3_Err | |
| 593 | 599 | } | |
| 594 | 600 | var templ_7745c5c3_Var31 string | |
| 595 | - | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | |
| 601 | + | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 596 | 602 | if templ_7745c5c3_Err != nil { | |
| 597 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 49} | |
| 603 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 125, Col: 60} | |
| 598 | 604 | } | |
| 599 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31) | |
| 605 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 600 | 606 | if templ_7745c5c3_Err != nil { | |
| 601 | 607 | return templ_7745c5c3_Err | |
| 602 | 608 | } | |
| 603 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">") | |
| 609 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " <time datetime=\"") | |
| 604 | 610 | if templ_7745c5c3_Err != nil { | |
| 605 | 611 | return templ_7745c5c3_Err | |
| 606 | 612 | } | |
| 607 | 613 | var templ_7745c5c3_Var32 string | |
| 608 | - | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(r.When) | |
| 614 | + | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | |
| 609 | 615 | if templ_7745c5c3_Err != nil { | |
| 610 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 60} | |
| 616 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 126, Col: 30} | |
| 611 | 617 | } | |
| 612 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | |
| 618 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) | |
| 613 | 619 | if templ_7745c5c3_Err != nil { | |
| 614 | 620 | return templ_7745c5c3_Err | |
| 615 | 621 | } | |
| 616 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</time>") | |
| 622 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" title=\"") | |
| 617 | 623 | if templ_7745c5c3_Err != nil { | |
| 618 | 624 | return templ_7745c5c3_Err | |
| 619 | 625 | } | |
| 620 | 626 | var templ_7745c5c3_Var33 string | |
| 621 | - | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 627 | + | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | |
| 622 | 628 | if templ_7745c5c3_Err != nil { | |
| 623 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 77} | |
| 629 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 126, Col: 49} | |
| 624 | 630 | } | |
| 625 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | |
| 631 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) | |
| 626 | 632 | if templ_7745c5c3_Err != nil { | |
| 627 | 633 | return templ_7745c5c3_Err | |
| 628 | 634 | } | |
| 629 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " <a href=\"") | |
| 635 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">") | |
| 630 | 636 | if templ_7745c5c3_Err != nil { | |
| 631 | 637 | return templ_7745c5c3_Err | |
| 632 | 638 | } | |
| 633 | - | var templ_7745c5c3_Var34 templ.SafeURL | |
| 634 | - | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.AllHref)) | |
| 639 | + | var templ_7745c5c3_Var34 string | |
| 640 | + | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(r.When) | |
| 635 | 641 | if templ_7745c5c3_Err != nil { | |
| 636 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 123, Col: 39} | |
| 642 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 126, Col: 60} | |
| 637 | 643 | } | |
| 638 | 644 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | |
| 639 | 645 | if templ_7745c5c3_Err != nil { | |
| 640 | 646 | return templ_7745c5c3_Err | |
| 641 | 647 | } | |
| 642 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">all releases</a></dd>") | |
| 648 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</time>") | |
| 649 | + | if templ_7745c5c3_Err != nil { | |
| 650 | + | return templ_7745c5c3_Err | |
| 651 | + | } | |
| 652 | + | var templ_7745c5c3_Var35 string | |
| 653 | + | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 654 | + | if templ_7745c5c3_Err != nil { | |
| 655 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 126, Col: 77} | |
| 656 | + | } | |
| 657 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 658 | + | if templ_7745c5c3_Err != nil { | |
| 659 | + | return templ_7745c5c3_Err | |
| 660 | + | } | |
| 661 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " <a href=\"") | |
| 662 | + | if templ_7745c5c3_Err != nil { | |
| 663 | + | return templ_7745c5c3_Err | |
| 664 | + | } | |
| 665 | + | var templ_7745c5c3_Var36 templ.SafeURL | |
| 666 | + | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.AllHref)) | |
| 667 | + | if templ_7745c5c3_Err != nil { | |
| 668 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 127, Col: 39} | |
| 669 | + | } | |
| 670 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | |
| 671 | + | if templ_7745c5c3_Err != nil { | |
| 672 | + | return templ_7745c5c3_Err | |
| 673 | + | } | |
| 674 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">all releases</a></dd>") | |
| 643 | 675 | if templ_7745c5c3_Err != nil { | |
| 644 | 676 | return templ_7745c5c3_Err | |
| 645 | 677 | } | |
| 646 | 678 | } | |
| 647 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</dl></section>") | |
| 679 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</dl></section>") | |
| 648 | 680 | if templ_7745c5c3_Err != nil { | |
| 649 | 681 | return templ_7745c5c3_Err | |
| 650 | 682 | } | |
| 651 | 683 | if len(p.Recent) > 0 { | |
| 652 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"") | |
| 684 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"") | |
| 653 | 685 | if templ_7745c5c3_Err != nil { | |
| 654 | 686 | return templ_7745c5c3_Err | |
| 655 | 687 | } | |
| 656 | - | var templ_7745c5c3_Var35 templ.SafeURL | |
| 657 | - | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref)) | |
| 688 | + | var templ_7745c5c3_Var37 templ.SafeURL | |
| 689 | + | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref)) | |
| 658 | 690 | if templ_7745c5c3_Err != nil { | |
| 659 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 129, Col: 105} | |
| 691 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 133, Col: 105} | |
| 660 | 692 | } | |
| 661 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 693 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) | |
| 662 | 694 | if templ_7745c5c3_Err != nil { | |
| 663 | 695 | return templ_7745c5c3_Err | |
| 664 | 696 | } | |
| 665 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">View all</a></h2>") | |
| 697 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">View all</a></h2>") | |
| 666 | 698 | if templ_7745c5c3_Err != nil { | |
| 667 | 699 | return templ_7745c5c3_Err | |
| 668 | 700 | } | |
| @@ -670,7 +702,7 @@ func summaryRail(p SummaryPage) templ.Component { | |||
| 670 | 702 | if templ_7745c5c3_Err != nil { | |
| 671 | 703 | return templ_7745c5c3_Err | |
| 672 | 704 | } | |
| 673 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</section>") | |
| 705 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</section>") | |
| 674 | 706 | if templ_7745c5c3_Err != nil { | |
| 675 | 707 | return templ_7745c5c3_Err | |
| 676 | 708 | } | |
| @@ -682,7 +714,7 @@ func summaryRail(p SummaryPage) templ.Component { | |||
| 682 | 714 | } | |
| 683 | 715 | } | |
| 684 | 716 | if len(p.Outline) > 0 && p.Readme != nil { | |
| 685 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>") | |
| 717 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>") | |
| 686 | 718 | if templ_7745c5c3_Err != nil { | |
| 687 | 719 | return templ_7745c5c3_Err | |
| 688 | 720 | } | |
| @@ -690,7 +722,7 @@ func summaryRail(p SummaryPage) templ.Component { | |||
| 690 | 722 | if templ_7745c5c3_Err != nil { | |
| 691 | 723 | return templ_7745c5c3_Err | |
| 692 | 724 | } | |
| 693 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</section>") | |
| 725 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</section>") | |
| 694 | 726 | if templ_7745c5c3_Err != nil { | |
| 695 | 727 | return templ_7745c5c3_Err | |
| 696 | 728 | } | |
| @@ -716,12 +748,12 @@ func Refs(l Layout, p RefsPage) templ.Component { | |||
| 716 | 748 | }() | |
| 717 | 749 | } | |
| 718 | 750 | ctx = templ.InitializeContext(ctx) | |
| 719 | - | templ_7745c5c3_Var36 := templ.GetChildren(ctx) | |
| 720 | - | if templ_7745c5c3_Var36 == nil { | |
| 721 | - | templ_7745c5c3_Var36 = templ.NopComponent | |
| 751 | + | templ_7745c5c3_Var38 := templ.GetChildren(ctx) | |
| 752 | + | if templ_7745c5c3_Var38 == nil { | |
| 753 | + | templ_7745c5c3_Var38 = templ.NopComponent | |
| 722 | 754 | } | |
| 723 | 755 | ctx = templ.ClearChildren(ctx) | |
| 724 | - | templ_7745c5c3_Var37 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 756 | + | templ_7745c5c3_Var39 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 725 | 757 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 726 | 758 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 727 | 759 | if !templ_7745c5c3_IsBuffer { | |
| @@ -733,7 +765,7 @@ func Refs(l Layout, p RefsPage) templ.Component { | |||
| 733 | 765 | }() | |
| 734 | 766 | } | |
| 735 | 767 | ctx = templ.InitializeContext(ctx) | |
| 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>") | |
| 768 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<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>") | |
| 737 | 769 | if templ_7745c5c3_Err != nil { | |
| 738 | 770 | return templ_7745c5c3_Err | |
| 739 | 771 | } | |
| @@ -741,32 +773,32 @@ func Refs(l Layout, p RefsPage) templ.Component { | |||
| 741 | 773 | if templ_7745c5c3_Err != nil { | |
| 742 | 774 | return templ_7745c5c3_Err | |
| 743 | 775 | } | |
| 744 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</table>") | |
| 776 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</table>") | |
| 745 | 777 | if templ_7745c5c3_Err != nil { | |
| 746 | 778 | return templ_7745c5c3_Err | |
| 747 | 779 | } | |
| 748 | 780 | if p.Note != "" { | |
| 749 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<p class=\"note\">") | |
| 781 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<p class=\"note\">") | |
| 750 | 782 | if templ_7745c5c3_Err != nil { | |
| 751 | 783 | return templ_7745c5c3_Err | |
| 752 | 784 | } | |
| 753 | - | var templ_7745c5c3_Var38 string | |
| 754 | - | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 785 | + | var templ_7745c5c3_Var40 string | |
| 786 | + | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 755 | 787 | if templ_7745c5c3_Err != nil { | |
| 756 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 162, Col: 27} | |
| 788 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 166, Col: 27} | |
| 757 | 789 | } | |
| 758 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) | |
| 790 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | |
| 759 | 791 | if templ_7745c5c3_Err != nil { | |
| 760 | 792 | return templ_7745c5c3_Err | |
| 761 | 793 | } | |
| 762 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</p>") | |
| 794 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</p>") | |
| 763 | 795 | if templ_7745c5c3_Err != nil { | |
| 764 | 796 | return templ_7745c5c3_Err | |
| 765 | 797 | } | |
| 766 | 798 | } | |
| 767 | 799 | return nil | |
| 768 | 800 | }) | |
| 769 | - | templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var37), templ_7745c5c3_Buffer) | |
| 801 | + | templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var39), templ_7745c5c3_Buffer) | |
| 770 | 802 | if templ_7745c5c3_Err != nil { | |
| 771 | 803 | return templ_7745c5c3_Err | |
| 772 | 804 | } | |
| @@ -790,112 +822,112 @@ func refRow(kind string, r RefRow) templ.Component { | |||
| 790 | 822 | }() | |
| 791 | 823 | } | |
| 792 | 824 | ctx = templ.InitializeContext(ctx) | |
| 793 | - | templ_7745c5c3_Var39 := templ.GetChildren(ctx) | |
| 794 | - | if templ_7745c5c3_Var39 == nil { | |
| 795 | - | templ_7745c5c3_Var39 = templ.NopComponent | |
| 825 | + | templ_7745c5c3_Var41 := templ.GetChildren(ctx) | |
| 826 | + | if templ_7745c5c3_Var41 == nil { | |
| 827 | + | templ_7745c5c3_Var41 = templ.NopComponent | |
| 796 | 828 | } | |
| 797 | 829 | ctx = templ.ClearChildren(ctx) | |
| 798 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<tr") | |
| 830 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<tr") | |
| 799 | 831 | if templ_7745c5c3_Err != nil { | |
| 800 | 832 | return templ_7745c5c3_Err | |
| 801 | 833 | } | |
| 802 | 834 | if r.Cursor { | |
| 803 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " data-cursor") | |
| 835 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " data-cursor") | |
| 804 | 836 | if templ_7745c5c3_Err != nil { | |
| 805 | 837 | return templ_7745c5c3_Err | |
| 806 | 838 | } | |
| 807 | 839 | } | |
| 808 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "><td class=\"c-kind mute\">") | |
| 840 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "><td class=\"c-kind mute\">") | |
| 809 | 841 | if templ_7745c5c3_Err != nil { | |
| 810 | 842 | return templ_7745c5c3_Err | |
| 811 | 843 | } | |
| 812 | - | var templ_7745c5c3_Var40 string | |
| 813 | - | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(kind) | |
| 844 | + | var templ_7745c5c3_Var42 string | |
| 845 | + | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(kind) | |
| 814 | 846 | if templ_7745c5c3_Err != nil { | |
| 815 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 169, Col: 32} | |
| 847 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 173, Col: 32} | |
| 816 | 848 | } | |
| 817 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | |
| 849 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | |
| 818 | 850 | if templ_7745c5c3_Err != nil { | |
| 819 | 851 | return templ_7745c5c3_Err | |
| 820 | 852 | } | |
| 821 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</td><td class=\"c-name\"><a href=\"") | |
| 853 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</td><td class=\"c-name\"><a href=\"") | |
| 822 | 854 | if templ_7745c5c3_Err != nil { | |
| 823 | 855 | return templ_7745c5c3_Err | |
| 824 | 856 | } | |
| 825 | - | var templ_7745c5c3_Var41 templ.SafeURL | |
| 826 | - | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | |
| 857 | + | var templ_7745c5c3_Var43 templ.SafeURL | |
| 858 | + | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | |
| 827 | 859 | if templ_7745c5c3_Err != nil { | |
| 828 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 171, Col: 34} | |
| 860 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 175, Col: 34} | |
| 829 | 861 | } | |
| 830 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | |
| 862 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | |
| 831 | 863 | if templ_7745c5c3_Err != nil { | |
| 832 | 864 | return templ_7745c5c3_Err | |
| 833 | 865 | } | |
| 834 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">") | |
| 866 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">") | |
| 835 | 867 | if templ_7745c5c3_Err != nil { | |
| 836 | 868 | return templ_7745c5c3_Err | |
| 837 | 869 | } | |
| 838 | - | var templ_7745c5c3_Var42 string | |
| 839 | - | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 870 | + | var templ_7745c5c3_Var44 string | |
| 871 | + | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 840 | 872 | if templ_7745c5c3_Err != nil { | |
| 841 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 171, Col: 45} | |
| 873 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 175, Col: 45} | |
| 842 | 874 | } | |
| 843 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | |
| 875 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | |
| 844 | 876 | if templ_7745c5c3_Err != nil { | |
| 845 | 877 | return templ_7745c5c3_Err | |
| 846 | 878 | } | |
| 847 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</a> ") | |
| 879 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</a> ") | |
| 848 | 880 | if templ_7745c5c3_Err != nil { | |
| 849 | 881 | return templ_7745c5c3_Err | |
| 850 | 882 | } | |
| 851 | 883 | if r.Default { | |
| 852 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<span class=\"pill\">default</span>") | |
| 884 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<span class=\"pill\">default</span>") | |
| 853 | 885 | if templ_7745c5c3_Err != nil { | |
| 854 | 886 | return templ_7745c5c3_Err | |
| 855 | 887 | } | |
| 856 | 888 | } | |
| 857 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</td><td class=\"c-sha\">") | |
| 889 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</td><td class=\"c-sha\">") | |
| 858 | 890 | if templ_7745c5c3_Err != nil { | |
| 859 | 891 | return templ_7745c5c3_Err | |
| 860 | 892 | } | |
| 861 | 893 | if r.CHref != "" { | |
| 862 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a href=\"") | |
| 894 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<a href=\"") | |
| 863 | 895 | if templ_7745c5c3_Err != nil { | |
| 864 | 896 | return templ_7745c5c3_Err | |
| 865 | 897 | } | |
| 866 | - | var templ_7745c5c3_Var43 templ.SafeURL | |
| 867 | - | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref)) | |
| 898 | + | var templ_7745c5c3_Var45 templ.SafeURL | |
| 899 | + | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref)) | |
| 868 | 900 | if templ_7745c5c3_Err != nil { | |
| 869 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 178, Col: 36} | |
| 901 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 182, Col: 36} | |
| 870 | 902 | } | |
| 871 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | |
| 903 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | |
| 872 | 904 | if templ_7745c5c3_Err != nil { | |
| 873 | 905 | return templ_7745c5c3_Err | |
| 874 | 906 | } | |
| 875 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\">") | |
| 907 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\">") | |
| 876 | 908 | if templ_7745c5c3_Err != nil { | |
| 877 | 909 | return templ_7745c5c3_Err | |
| 878 | 910 | } | |
| 879 | - | var templ_7745c5c3_Var44 string | |
| 880 | - | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short) | |
| 911 | + | var templ_7745c5c3_Var46 string | |
| 912 | + | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short) | |
| 881 | 913 | if templ_7745c5c3_Err != nil { | |
| 882 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 178, Col: 48} | |
| 914 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 182, Col: 48} | |
| 883 | 915 | } | |
| 884 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | |
| 916 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | |
| 885 | 917 | if templ_7745c5c3_Err != nil { | |
| 886 | 918 | return templ_7745c5c3_Err | |
| 887 | 919 | } | |
| 888 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</a>") | |
| 920 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</a>") | |
| 889 | 921 | if templ_7745c5c3_Err != nil { | |
| 890 | 922 | return templ_7745c5c3_Err | |
| 891 | 923 | } | |
| 892 | 924 | } else { | |
| 893 | - | var templ_7745c5c3_Var45 string | |
| 894 | - | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short) | |
| 925 | + | var templ_7745c5c3_Var47 string | |
| 926 | + | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short) | |
| 895 | 927 | if templ_7745c5c3_Err != nil { | |
| 896 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 180, Col: 13} | |
| 928 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 184, Col: 13} | |
| 897 | 929 | } | |
| 898 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | |
| 930 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) | |
| 899 | 931 | if templ_7745c5c3_Err != nil { | |
| 900 | 932 | return templ_7745c5c3_Err | |
| 901 | 933 | } | |
| @@ -904,69 +936,69 @@ func refRow(kind string, r RefRow) templ.Component { | |||
| 904 | 936 | if templ_7745c5c3_Err != nil { | |
| 905 | 937 | return templ_7745c5c3_Err | |
| 906 | 938 | } | |
| 907 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</td><td class=\"c-sub\">") | |
| 939 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</td><td class=\"c-sub\">") | |
| 908 | 940 | if templ_7745c5c3_Err != nil { | |
| 909 | 941 | return templ_7745c5c3_Err | |
| 910 | 942 | } | |
| 911 | - | var templ_7745c5c3_Var46 string | |
| 912 | - | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject) | |
| 943 | + | var templ_7745c5c3_Var48 string | |
| 944 | + | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject) | |
| 913 | 945 | if templ_7745c5c3_Err != nil { | |
| 914 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 184, Col: 31} | |
| 946 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 188, Col: 31} | |
| 915 | 947 | } | |
| 916 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | |
| 948 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | |
| 917 | 949 | if templ_7745c5c3_Err != nil { | |
| 918 | 950 | return templ_7745c5c3_Err | |
| 919 | 951 | } | |
| 920 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</td><td class=\"c-when dim\">") | |
| 952 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</td><td class=\"c-when dim\">") | |
| 921 | 953 | if templ_7745c5c3_Err != nil { | |
| 922 | 954 | return templ_7745c5c3_Err | |
| 923 | 955 | } | |
| 924 | 956 | if r.WhenAt != "" { | |
| 925 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<time datetime=\"") | |
| 957 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<time datetime=\"") | |
| 926 | 958 | if templ_7745c5c3_Err != nil { | |
| 927 | 959 | return templ_7745c5c3_Err | |
| 928 | 960 | } | |
| 929 | - | var templ_7745c5c3_Var47 string | |
| 930 | - | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | |
| 961 | + | var templ_7745c5c3_Var49 string | |
| 962 | + | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | |
| 931 | 963 | if templ_7745c5c3_Err != nil { | |
| 932 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 29} | |
| 964 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 191, Col: 29} | |
| 933 | 965 | } | |
| 934 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47) | |
| 966 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49) | |
| 935 | 967 | if templ_7745c5c3_Err != nil { | |
| 936 | 968 | return templ_7745c5c3_Err | |
| 937 | 969 | } | |
| 938 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" title=\"") | |
| 970 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\" title=\"") | |
| 939 | 971 | if templ_7745c5c3_Err != nil { | |
| 940 | 972 | return templ_7745c5c3_Err | |
| 941 | 973 | } | |
| 942 | - | var templ_7745c5c3_Var48 string | |
| 943 | - | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | |
| 974 | + | var templ_7745c5c3_Var50 string | |
| 975 | + | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | |
| 944 | 976 | if templ_7745c5c3_Err != nil { | |
| 945 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 48} | |
| 977 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 191, Col: 48} | |
| 946 | 978 | } | |
| 947 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48) | |
| 979 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50) | |
| 948 | 980 | if templ_7745c5c3_Err != nil { | |
| 949 | 981 | return templ_7745c5c3_Err | |
| 950 | 982 | } | |
| 951 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\">") | |
| 983 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\">") | |
| 952 | 984 | if templ_7745c5c3_Err != nil { | |
| 953 | 985 | return templ_7745c5c3_Err | |
| 954 | 986 | } | |
| 955 | - | var templ_7745c5c3_Var49 string | |
| 956 | - | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(r.When) | |
| 987 | + | var templ_7745c5c3_Var51 string | |
| 988 | + | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(r.When) | |
| 957 | 989 | if templ_7745c5c3_Err != nil { | |
| 958 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 59} | |
| 990 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 191, Col: 59} | |
| 959 | 991 | } | |
| 960 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) | |
| 992 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | |
| 961 | 993 | if templ_7745c5c3_Err != nil { | |
| 962 | 994 | return templ_7745c5c3_Err | |
| 963 | 995 | } | |
| 964 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</time>") | |
| 996 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</time>") | |
| 965 | 997 | if templ_7745c5c3_Err != nil { | |
| 966 | 998 | return templ_7745c5c3_Err | |
| 967 | 999 | } | |
| 968 | 1000 | } | |
| 969 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</td></tr>") | |
| 1001 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</td></tr>") | |
| 970 | 1002 | if templ_7745c5c3_Err != nil { | |
| 971 | 1003 | return templ_7745c5c3_Err | |
| 972 | 1004 | } | |
| @@ -990,12 +1022,12 @@ func refsRail(p RefsPage) templ.Component { | |||
| 990 | 1022 | }() | |
| 991 | 1023 | } | |
| 992 | 1024 | ctx = templ.InitializeContext(ctx) | |
| 993 | - | templ_7745c5c3_Var50 := templ.GetChildren(ctx) | |
| 994 | - | if templ_7745c5c3_Var50 == nil { | |
| 995 | - | templ_7745c5c3_Var50 = templ.NopComponent | |
| 1025 | + | templ_7745c5c3_Var52 := templ.GetChildren(ctx) | |
| 1026 | + | if templ_7745c5c3_Var52 == nil { | |
| 1027 | + | templ_7745c5c3_Var52 = templ.NopComponent | |
| 996 | 1028 | } | |
| 997 | 1029 | ctx = templ.ClearChildren(ctx) | |
| 998 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>") | |
| 1030 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>") | |
| 999 | 1031 | if templ_7745c5c3_Err != nil { | |
| 1000 | 1032 | return templ_7745c5c3_Err | |
| 1001 | 1033 | } | |
| @@ -1003,7 +1035,7 @@ func refsRail(p RefsPage) templ.Component { | |||
| 1003 | 1035 | if templ_7745c5c3_Err != nil { | |
| 1004 | 1036 | return templ_7745c5c3_Err | |
| 1005 | 1037 | } | |
| 1006 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</section>") | |
| 1038 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</section>") | |
| 1007 | 1039 | if templ_7745c5c3_Err != nil { | |
| 1008 | 1040 | return templ_7745c5c3_Err | |
| 1009 | 1041 | } | |
| @@ -1028,12 +1060,12 @@ func State(l Layout, p StatePage) templ.Component { | |||
| 1028 | 1060 | }() | |
| 1029 | 1061 | } | |
| 1030 | 1062 | ctx = templ.InitializeContext(ctx) | |
| 1031 | - | templ_7745c5c3_Var51 := templ.GetChildren(ctx) | |
| 1032 | - | if templ_7745c5c3_Var51 == nil { | |
| 1033 | - | templ_7745c5c3_Var51 = templ.NopComponent | |
| 1063 | + | templ_7745c5c3_Var53 := templ.GetChildren(ctx) | |
| 1064 | + | if templ_7745c5c3_Var53 == nil { | |
| 1065 | + | templ_7745c5c3_Var53 = templ.NopComponent | |
| 1034 | 1066 | } | |
| 1035 | 1067 | ctx = templ.ClearChildren(ctx) | |
| 1036 | - | templ_7745c5c3_Var52 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1068 | + | templ_7745c5c3_Var54 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1037 | 1069 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1038 | 1070 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1039 | 1071 | if !templ_7745c5c3_IsBuffer { | |
| @@ -1045,51 +1077,51 @@ func State(l Layout, p StatePage) templ.Component { | |||
| 1045 | 1077 | }() | |
| 1046 | 1078 | } | |
| 1047 | 1079 | ctx = templ.InitializeContext(ctx) | |
| 1048 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">") | |
| 1080 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">") | |
| 1049 | 1081 | if templ_7745c5c3_Err != nil { | |
| 1050 | 1082 | return templ_7745c5c3_Err | |
| 1051 | 1083 | } | |
| 1052 | - | var templ_7745c5c3_Var53 string | |
| 1053 | - | templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code) | |
| 1084 | + | var templ_7745c5c3_Var55 string | |
| 1085 | + | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code) | |
| 1054 | 1086 | if templ_7745c5c3_Err != nil { | |
| 1055 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 204, Col: 30} | |
| 1087 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 208, Col: 30} | |
| 1056 | 1088 | } | |
| 1057 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) | |
| 1089 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 1058 | 1090 | if templ_7745c5c3_Err != nil { | |
| 1059 | 1091 | return templ_7745c5c3_Err | |
| 1060 | 1092 | } | |
| 1061 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</p><h1 id=\"state-title\">") | |
| 1093 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</p><h1 id=\"state-title\">") | |
| 1062 | 1094 | if templ_7745c5c3_Err != nil { | |
| 1063 | 1095 | return templ_7745c5c3_Err | |
| 1064 | 1096 | } | |
| 1065 | - | var templ_7745c5c3_Var54 string | |
| 1066 | - | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | |
| 1097 | + | var templ_7745c5c3_Var56 string | |
| 1098 | + | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | |
| 1067 | 1099 | if templ_7745c5c3_Err != nil { | |
| 1068 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 205, Col: 33} | |
| 1100 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 209, Col: 33} | |
| 1069 | 1101 | } | |
| 1070 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | |
| 1102 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) | |
| 1071 | 1103 | if templ_7745c5c3_Err != nil { | |
| 1072 | 1104 | return templ_7745c5c3_Err | |
| 1073 | 1105 | } | |
| 1074 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</h1>") | |
| 1106 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</h1>") | |
| 1075 | 1107 | if templ_7745c5c3_Err != nil { | |
| 1076 | 1108 | return templ_7745c5c3_Err | |
| 1077 | 1109 | } | |
| 1078 | 1110 | for _, line := range p.Lines { | |
| 1079 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<p class=\"dim\">") | |
| 1111 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<p class=\"dim\">") | |
| 1080 | 1112 | if templ_7745c5c3_Err != nil { | |
| 1081 | 1113 | return templ_7745c5c3_Err | |
| 1082 | 1114 | } | |
| 1083 | - | var templ_7745c5c3_Var55 string | |
| 1084 | - | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(line) | |
| 1115 | + | var templ_7745c5c3_Var57 string | |
| 1116 | + | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(line) | |
| 1085 | 1117 | if templ_7745c5c3_Err != nil { | |
| 1086 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 207, Col: 25} | |
| 1118 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 211, Col: 25} | |
| 1087 | 1119 | } | |
| 1088 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 1120 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) | |
| 1089 | 1121 | if templ_7745c5c3_Err != nil { | |
| 1090 | 1122 | return templ_7745c5c3_Err | |
| 1091 | 1123 | } | |
| 1092 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</p>") | |
| 1124 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</p>") | |
| 1093 | 1125 | if templ_7745c5c3_Err != nil { | |
| 1094 | 1126 | return templ_7745c5c3_Err | |
| 1095 | 1127 | } | |
| @@ -1100,49 +1132,49 @@ func State(l Layout, p StatePage) templ.Component { | |||
| 1100 | 1132 | return templ_7745c5c3_Err | |
| 1101 | 1133 | } | |
| 1102 | 1134 | } | |
| 1103 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<p class=\"state-back\">") | |
| 1135 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<p class=\"state-back\">") | |
| 1104 | 1136 | if templ_7745c5c3_Err != nil { | |
| 1105 | 1137 | return templ_7745c5c3_Err | |
| 1106 | 1138 | } | |
| 1107 | 1139 | for _, b := range p.Back { | |
| 1108 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<a href=\"") | |
| 1140 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<a href=\"") | |
| 1109 | 1141 | if templ_7745c5c3_Err != nil { | |
| 1110 | 1142 | return templ_7745c5c3_Err | |
| 1111 | 1143 | } | |
| 1112 | - | var templ_7745c5c3_Var56 templ.SafeURL | |
| 1113 | - | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href)) | |
| 1144 | + | var templ_7745c5c3_Var58 templ.SafeURL | |
| 1145 | + | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href)) | |
| 1114 | 1146 | if templ_7745c5c3_Err != nil { | |
| 1115 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 214, Col: 36} | |
| 1147 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 218, Col: 36} | |
| 1116 | 1148 | } | |
| 1117 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) | |
| 1149 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) | |
| 1118 | 1150 | if templ_7745c5c3_Err != nil { | |
| 1119 | 1151 | return templ_7745c5c3_Err | |
| 1120 | 1152 | } | |
| 1121 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "\">") | |
| 1153 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\">") | |
| 1122 | 1154 | if templ_7745c5c3_Err != nil { | |
| 1123 | 1155 | return templ_7745c5c3_Err | |
| 1124 | 1156 | } | |
| 1125 | - | var templ_7745c5c3_Var57 string | |
| 1126 | - | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label) | |
| 1157 | + | var templ_7745c5c3_Var59 string | |
| 1158 | + | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label) | |
| 1127 | 1159 | if templ_7745c5c3_Err != nil { | |
| 1128 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 214, Col: 48} | |
| 1160 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 218, Col: 48} | |
| 1129 | 1161 | } | |
| 1130 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) | |
| 1162 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) | |
| 1131 | 1163 | if templ_7745c5c3_Err != nil { | |
| 1132 | 1164 | return templ_7745c5c3_Err | |
| 1133 | 1165 | } | |
| 1134 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</a>") | |
| 1166 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</a>") | |
| 1135 | 1167 | if templ_7745c5c3_Err != nil { | |
| 1136 | 1168 | return templ_7745c5c3_Err | |
| 1137 | 1169 | } | |
| 1138 | 1170 | } | |
| 1139 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</p></section>") | |
| 1171 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</p></section>") | |
| 1140 | 1172 | if templ_7745c5c3_Err != nil { | |
| 1141 | 1173 | return templ_7745c5c3_Err | |
| 1142 | 1174 | } | |
| 1143 | 1175 | return nil | |
| 1144 | 1176 | }) | |
| 1145 | - | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var52), templ_7745c5c3_Buffer) | |
| 1177 | + | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var54), templ_7745c5c3_Buffer) | |
| 1146 | 1178 | if templ_7745c5c3_Err != nil { | |
| 1147 | 1179 | return templ_7745c5c3_Err | |
| 1148 | 1180 | } | |
| @@ -1171,12 +1203,12 @@ func Login(l Layout, p LoginPage) templ.Component { | |||
| 1171 | 1203 | }() | |
| 1172 | 1204 | } | |
| 1173 | 1205 | ctx = templ.InitializeContext(ctx) | |
| 1174 | - | templ_7745c5c3_Var58 := templ.GetChildren(ctx) | |
| 1175 | - | if templ_7745c5c3_Var58 == nil { | |
| 1176 | - | templ_7745c5c3_Var58 = templ.NopComponent | |
| 1206 | + | templ_7745c5c3_Var60 := templ.GetChildren(ctx) | |
| 1207 | + | if templ_7745c5c3_Var60 == nil { | |
| 1208 | + | templ_7745c5c3_Var60 = templ.NopComponent | |
| 1177 | 1209 | } | |
| 1178 | 1210 | ctx = templ.ClearChildren(ctx) | |
| 1179 | - | templ_7745c5c3_Var59 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1211 | + | templ_7745c5c3_Var61 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1180 | 1212 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1181 | 1213 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1182 | 1214 | if !templ_7745c5c3_IsBuffer { | |
| @@ -1188,75 +1220,75 @@ func Login(l Layout, p LoginPage) templ.Component { | |||
| 1188 | 1220 | }() | |
| 1189 | 1221 | } | |
| 1190 | 1222 | ctx = templ.InitializeContext(ctx) | |
| 1191 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">") | |
| 1223 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">") | |
| 1192 | 1224 | if templ_7745c5c3_Err != nil { | |
| 1193 | 1225 | return templ_7745c5c3_Err | |
| 1194 | 1226 | } | |
| 1195 | - | var templ_7745c5c3_Var60 string | |
| 1196 | - | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | |
| 1227 | + | var templ_7745c5c3_Var62 string | |
| 1228 | + | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | |
| 1197 | 1229 | if templ_7745c5c3_Err != nil { | |
| 1198 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 229, Col: 33} | |
| 1230 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 233, Col: 33} | |
| 1199 | 1231 | } | |
| 1200 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) | |
| 1232 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | |
| 1201 | 1233 | if templ_7745c5c3_Err != nil { | |
| 1202 | 1234 | return templ_7745c5c3_Err | |
| 1203 | 1235 | } | |
| 1204 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</h1>") | |
| 1236 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</h1>") | |
| 1205 | 1237 | if templ_7745c5c3_Err != nil { | |
| 1206 | 1238 | return templ_7745c5c3_Err | |
| 1207 | 1239 | } | |
| 1208 | 1240 | for _, line := range p.Lines { | |
| 1209 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<p class=\"dim\">") | |
| 1241 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<p class=\"dim\">") | |
| 1210 | 1242 | if templ_7745c5c3_Err != nil { | |
| 1211 | 1243 | return templ_7745c5c3_Err | |
| 1212 | 1244 | } | |
| 1213 | - | var templ_7745c5c3_Var61 string | |
| 1214 | - | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(line) | |
| 1245 | + | var templ_7745c5c3_Var63 string | |
| 1246 | + | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(line) | |
| 1215 | 1247 | if templ_7745c5c3_Err != nil { | |
| 1216 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 231, Col: 25} | |
| 1248 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 235, Col: 25} | |
| 1217 | 1249 | } | |
| 1218 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | |
| 1250 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) | |
| 1219 | 1251 | if templ_7745c5c3_Err != nil { | |
| 1220 | 1252 | return templ_7745c5c3_Err | |
| 1221 | 1253 | } | |
| 1222 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</p>") | |
| 1254 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</p>") | |
| 1223 | 1255 | if templ_7745c5c3_Err != nil { | |
| 1224 | 1256 | return templ_7745c5c3_Err | |
| 1225 | 1257 | } | |
| 1226 | 1258 | } | |
| 1227 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "<form class=\"state-act\" method=\"post\" action=\"") | |
| 1259 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<form class=\"state-act\" method=\"post\" action=\"") | |
| 1228 | 1260 | if templ_7745c5c3_Err != nil { | |
| 1229 | 1261 | return templ_7745c5c3_Err | |
| 1230 | 1262 | } | |
| 1231 | - | var templ_7745c5c3_Var62 templ.SafeURL | |
| 1232 | - | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action)) | |
| 1263 | + | var templ_7745c5c3_Var64 templ.SafeURL | |
| 1264 | + | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action)) | |
| 1233 | 1265 | if templ_7745c5c3_Err != nil { | |
| 1234 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 233, Col: 73} | |
| 1266 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 237, Col: 73} | |
| 1235 | 1267 | } | |
| 1236 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | |
| 1268 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64)) | |
| 1237 | 1269 | if templ_7745c5c3_Err != nil { | |
| 1238 | 1270 | return templ_7745c5c3_Err | |
| 1239 | 1271 | } | |
| 1240 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">") | |
| 1272 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">") | |
| 1241 | 1273 | if templ_7745c5c3_Err != nil { | |
| 1242 | 1274 | return templ_7745c5c3_Err | |
| 1243 | 1275 | } | |
| 1244 | - | var templ_7745c5c3_Var63 string | |
| 1245 | - | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button) | |
| 1276 | + | var templ_7745c5c3_Var65 string | |
| 1277 | + | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button) | |
| 1246 | 1278 | if templ_7745c5c3_Err != nil { | |
| 1247 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 234, Col: 56} | |
| 1279 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 238, Col: 56} | |
| 1248 | 1280 | } | |
| 1249 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) | |
| 1281 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | |
| 1250 | 1282 | if templ_7745c5c3_Err != nil { | |
| 1251 | 1283 | return templ_7745c5c3_Err | |
| 1252 | 1284 | } | |
| 1253 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>") | |
| 1285 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>") | |
| 1254 | 1286 | if templ_7745c5c3_Err != nil { | |
| 1255 | 1287 | return templ_7745c5c3_Err | |
| 1256 | 1288 | } | |
| 1257 | 1289 | return nil | |
| 1258 | 1290 | }) | |
| 1259 | - | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var59), templ_7745c5c3_Buffer) | |
| 1291 | + | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var61), templ_7745c5c3_Buffer) | |
| 1260 | 1292 | if templ_7745c5c3_Err != nil { | |
| 1261 | 1293 | return templ_7745c5c3_Err | |
| 1262 | 1294 | } | |
| @@ -1282,12 +1314,12 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component { | |||
| 1282 | 1314 | }() | |
| 1283 | 1315 | } | |
| 1284 | 1316 | ctx = templ.InitializeContext(ctx) | |
| 1285 | - | templ_7745c5c3_Var64 := templ.GetChildren(ctx) | |
| 1286 | - | if templ_7745c5c3_Var64 == nil { | |
| 1287 | - | templ_7745c5c3_Var64 = templ.NopComponent | |
| 1317 | + | templ_7745c5c3_Var66 := templ.GetChildren(ctx) | |
| 1318 | + | if templ_7745c5c3_Var66 == nil { | |
| 1319 | + | templ_7745c5c3_Var66 = templ.NopComponent | |
| 1288 | 1320 | } | |
| 1289 | 1321 | ctx = templ.ClearChildren(ctx) | |
| 1290 | - | templ_7745c5c3_Var65 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1322 | + | templ_7745c5c3_Var67 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1291 | 1323 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1292 | 1324 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1293 | 1325 | if !templ_7745c5c3_IsBuffer { | |
| @@ -1299,88 +1331,88 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component { | |||
| 1299 | 1331 | }() | |
| 1300 | 1332 | } | |
| 1301 | 1333 | ctx = templ.InitializeContext(ctx) | |
| 1302 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">") | |
| 1334 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">") | |
| 1303 | 1335 | if templ_7745c5c3_Err != nil { | |
| 1304 | 1336 | return templ_7745c5c3_Err | |
| 1305 | 1337 | } | |
| 1306 | - | var templ_7745c5c3_Var66 string | |
| 1307 | - | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | |
| 1338 | + | var templ_7745c5c3_Var68 string | |
| 1339 | + | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | |
| 1308 | 1340 | if templ_7745c5c3_Err != nil { | |
| 1309 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 246, Col: 33} | |
| 1341 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 250, Col: 33} | |
| 1310 | 1342 | } | |
| 1311 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) | |
| 1343 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | |
| 1312 | 1344 | if templ_7745c5c3_Err != nil { | |
| 1313 | 1345 | return templ_7745c5c3_Err | |
| 1314 | 1346 | } | |
| 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=\"") | |
| 1347 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "</h1><form class=\"state-act\" data-js data-passkey-form><button type=\"submit\" class=\"btn primary\" data-passkey=\"login\" data-begin=\"") | |
| 1316 | 1348 | if templ_7745c5c3_Err != nil { | |
| 1317 | 1349 | return templ_7745c5c3_Err | |
| 1318 | 1350 | } | |
| 1319 | - | var templ_7745c5c3_Var67 string | |
| 1320 | - | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin) | |
| 1351 | + | var templ_7745c5c3_Var69 string | |
| 1352 | + | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin) | |
| 1321 | 1353 | if templ_7745c5c3_Err != nil { | |
| 1322 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 87} | |
| 1354 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 252, Col: 87} | |
| 1323 | 1355 | } | |
| 1324 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67) | |
| 1356 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69) | |
| 1325 | 1357 | if templ_7745c5c3_Err != nil { | |
| 1326 | 1358 | return templ_7745c5c3_Err | |
| 1327 | 1359 | } | |
| 1328 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "\" data-finish=\"") | |
| 1360 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "\" data-finish=\"") | |
| 1329 | 1361 | if templ_7745c5c3_Err != nil { | |
| 1330 | 1362 | return templ_7745c5c3_Err | |
| 1331 | 1363 | } | |
| 1332 | - | var templ_7745c5c3_Var68 string | |
| 1333 | - | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish) | |
| 1364 | + | var templ_7745c5c3_Var70 string | |
| 1365 | + | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish) | |
| 1334 | 1366 | if templ_7745c5c3_Err != nil { | |
| 1335 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 112} | |
| 1367 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 252, Col: 112} | |
| 1336 | 1368 | } | |
| 1337 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var68) | |
| 1369 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var70) | |
| 1338 | 1370 | if templ_7745c5c3_Err != nil { | |
| 1339 | 1371 | return templ_7745c5c3_Err | |
| 1340 | 1372 | } | |
| 1341 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "\">") | |
| 1373 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "\">") | |
| 1342 | 1374 | if templ_7745c5c3_Err != nil { | |
| 1343 | 1375 | return templ_7745c5c3_Err | |
| 1344 | 1376 | } | |
| 1345 | - | var templ_7745c5c3_Var69 string | |
| 1346 | - | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button) | |
| 1377 | + | var templ_7745c5c3_Var71 string | |
| 1378 | + | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button) | |
| 1347 | 1379 | if templ_7745c5c3_Err != nil { | |
| 1348 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 125} | |
| 1380 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 252, Col: 125} | |
| 1349 | 1381 | } | |
| 1350 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | |
| 1382 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | |
| 1351 | 1383 | if templ_7745c5c3_Err != nil { | |
| 1352 | 1384 | return templ_7745c5c3_Err | |
| 1353 | 1385 | } | |
| 1354 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>") | |
| 1386 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>") | |
| 1355 | 1387 | if templ_7745c5c3_Err != nil { | |
| 1356 | 1388 | return templ_7745c5c3_Err | |
| 1357 | 1389 | } | |
| 1358 | 1390 | for _, line := range p.Lines { | |
| 1359 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<p class=\"dim\">") | |
| 1391 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<p class=\"dim\">") | |
| 1360 | 1392 | if templ_7745c5c3_Err != nil { | |
| 1361 | 1393 | return templ_7745c5c3_Err | |
| 1362 | 1394 | } | |
| 1363 | - | var templ_7745c5c3_Var70 string | |
| 1364 | - | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(line) | |
| 1395 | + | var templ_7745c5c3_Var72 string | |
| 1396 | + | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(line) | |
| 1365 | 1397 | if templ_7745c5c3_Err != nil { | |
| 1366 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 252, Col: 25} | |
| 1398 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 256, Col: 25} | |
| 1367 | 1399 | } | |
| 1368 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | |
| 1400 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) | |
| 1369 | 1401 | if templ_7745c5c3_Err != nil { | |
| 1370 | 1402 | return templ_7745c5c3_Err | |
| 1371 | 1403 | } | |
| 1372 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "</p>") | |
| 1404 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</p>") | |
| 1373 | 1405 | if templ_7745c5c3_Err != nil { | |
| 1374 | 1406 | return templ_7745c5c3_Err | |
| 1375 | 1407 | } | |
| 1376 | 1408 | } | |
| 1377 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>") | |
| 1409 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>") | |
| 1378 | 1410 | if templ_7745c5c3_Err != nil { | |
| 1379 | 1411 | return templ_7745c5c3_Err | |
| 1380 | 1412 | } | |
| 1381 | 1413 | return nil | |
| 1382 | 1414 | }) | |
| 1383 | - | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var65), templ_7745c5c3_Buffer) | |
| 1415 | + | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var67), templ_7745c5c3_Buffer) | |
| 1384 | 1416 | if templ_7745c5c3_Err != nil { | |
| 1385 | 1417 | return templ_7745c5c3_Err | |
| 1386 | 1418 | } | |
| @@ -1405,12 +1437,12 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component { | |||
| 1405 | 1437 | }() | |
| 1406 | 1438 | } | |
| 1407 | 1439 | ctx = templ.InitializeContext(ctx) | |
| 1408 | - | templ_7745c5c3_Var71 := templ.GetChildren(ctx) | |
| 1409 | - | if templ_7745c5c3_Var71 == nil { | |
| 1410 | - | templ_7745c5c3_Var71 = templ.NopComponent | |
| 1440 | + | templ_7745c5c3_Var73 := templ.GetChildren(ctx) | |
| 1441 | + | if templ_7745c5c3_Var73 == nil { | |
| 1442 | + | templ_7745c5c3_Var73 = templ.NopComponent | |
| 1411 | 1443 | } | |
| 1412 | 1444 | ctx = templ.ClearChildren(ctx) | |
| 1413 | - | templ_7745c5c3_Var72 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1445 | + | templ_7745c5c3_Var74 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1414 | 1446 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1415 | 1447 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1416 | 1448 | if !templ_7745c5c3_IsBuffer { | |
| @@ -1422,231 +1454,231 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component { | |||
| 1422 | 1454 | }() | |
| 1423 | 1455 | } | |
| 1424 | 1456 | ctx = templ.InitializeContext(ctx) | |
| 1425 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">") | |
| 1457 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">") | |
| 1426 | 1458 | if templ_7745c5c3_Err != nil { | |
| 1427 | 1459 | return templ_7745c5c3_Err | |
| 1428 | 1460 | } | |
| 1429 | - | var templ_7745c5c3_Var73 string | |
| 1430 | - | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows))) | |
| 1461 | + | var templ_7745c5c3_Var75 string | |
| 1462 | + | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows))) | |
| 1431 | 1463 | if templ_7745c5c3_Err != nil { | |
| 1432 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 262, Col: 81} | |
| 1464 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 266, Col: 81} | |
| 1433 | 1465 | } | |
| 1434 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) | |
| 1466 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) | |
| 1435 | 1467 | if templ_7745c5c3_Err != nil { | |
| 1436 | 1468 | return templ_7745c5c3_Err | |
| 1437 | 1469 | } | |
| 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>") | |
| 1470 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</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>") | |
| 1439 | 1471 | if templ_7745c5c3_Err != nil { | |
| 1440 | 1472 | return templ_7745c5c3_Err | |
| 1441 | 1473 | } | |
| 1442 | 1474 | if len(p.Rows) > 0 { | |
| 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>") | |
| 1475 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<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>") | |
| 1444 | 1476 | if templ_7745c5c3_Err != nil { | |
| 1445 | 1477 | return templ_7745c5c3_Err | |
| 1446 | 1478 | } | |
| 1447 | 1479 | for _, r := range p.Rows { | |
| 1448 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "<tr><td class=\"c-name\">") | |
| 1480 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "<tr><td class=\"c-name\">") | |
| 1449 | 1481 | if templ_7745c5c3_Err != nil { | |
| 1450 | 1482 | return templ_7745c5c3_Err | |
| 1451 | 1483 | } | |
| 1452 | - | var templ_7745c5c3_Var74 string | |
| 1453 | - | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 1484 | + | var templ_7745c5c3_Var76 string | |
| 1485 | + | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 1454 | 1486 | if templ_7745c5c3_Err != nil { | |
| 1455 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 278, Col: 34} | |
| 1487 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 282, Col: 34} | |
| 1456 | 1488 | } | |
| 1457 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74)) | |
| 1489 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) | |
| 1458 | 1490 | if templ_7745c5c3_Err != nil { | |
| 1459 | 1491 | return templ_7745c5c3_Err | |
| 1460 | 1492 | } | |
| 1461 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</td><td class=\"c-when nowrap\"><time") | |
| 1493 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</td><td class=\"c-when nowrap\"><time") | |
| 1462 | 1494 | if templ_7745c5c3_Err != nil { | |
| 1463 | 1495 | return templ_7745c5c3_Err | |
| 1464 | 1496 | } | |
| 1465 | 1497 | if r.CreatedAt != "" { | |
| 1466 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, " datetime=\"") | |
| 1498 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, " datetime=\"") | |
| 1467 | 1499 | if templ_7745c5c3_Err != nil { | |
| 1468 | 1500 | return templ_7745c5c3_Err | |
| 1469 | 1501 | } | |
| 1470 | - | var templ_7745c5c3_Var75 string | |
| 1471 | - | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt) | |
| 1502 | + | var templ_7745c5c3_Var77 string | |
| 1503 | + | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt) | |
| 1472 | 1504 | if templ_7745c5c3_Err != nil { | |
| 1473 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 282, Col: 32} | |
| 1505 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 286, Col: 32} | |
| 1474 | 1506 | } | |
| 1475 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75) | |
| 1507 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77) | |
| 1476 | 1508 | if templ_7745c5c3_Err != nil { | |
| 1477 | 1509 | return templ_7745c5c3_Err | |
| 1478 | 1510 | } | |
| 1479 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"") | |
| 1511 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\"") | |
| 1480 | 1512 | if templ_7745c5c3_Err != nil { | |
| 1481 | 1513 | return templ_7745c5c3_Err | |
| 1482 | 1514 | } | |
| 1483 | 1515 | } | |
| 1484 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, ">") | |
| 1516 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, ">") | |
| 1485 | 1517 | if templ_7745c5c3_Err != nil { | |
| 1486 | 1518 | return templ_7745c5c3_Err | |
| 1487 | 1519 | } | |
| 1488 | - | var templ_7745c5c3_Var76 string | |
| 1489 | - | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created) | |
| 1520 | + | var templ_7745c5c3_Var78 string | |
| 1521 | + | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created) | |
| 1490 | 1522 | if templ_7745c5c3_Err != nil { | |
| 1491 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 284, Col: 20} | |
| 1523 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 288, Col: 20} | |
| 1492 | 1524 | } | |
| 1493 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) | |
| 1525 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) | |
| 1494 | 1526 | if templ_7745c5c3_Err != nil { | |
| 1495 | 1527 | return templ_7745c5c3_Err | |
| 1496 | 1528 | } | |
| 1497 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "</time></td><td class=\"c-when nowrap\"><time") | |
| 1529 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</time></td><td class=\"c-when nowrap\"><time") | |
| 1498 | 1530 | if templ_7745c5c3_Err != nil { | |
| 1499 | 1531 | return templ_7745c5c3_Err | |
| 1500 | 1532 | } | |
| 1501 | 1533 | if r.LastUsedAt != "" { | |
| 1502 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, " datetime=\"") | |
| 1534 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, " datetime=\"") | |
| 1503 | 1535 | if templ_7745c5c3_Err != nil { | |
| 1504 | 1536 | return templ_7745c5c3_Err | |
| 1505 | 1537 | } | |
| 1506 | - | var templ_7745c5c3_Var77 string | |
| 1507 | - | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt) | |
| 1538 | + | var templ_7745c5c3_Var79 string | |
| 1539 | + | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt) | |
| 1508 | 1540 | if templ_7745c5c3_Err != nil { | |
| 1509 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 289, Col: 33} | |
| 1541 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 293, Col: 33} | |
| 1510 | 1542 | } | |
| 1511 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77) | |
| 1543 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79) | |
| 1512 | 1544 | if templ_7745c5c3_Err != nil { | |
| 1513 | 1545 | return templ_7745c5c3_Err | |
| 1514 | 1546 | } | |
| 1515 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\"") | |
| 1547 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\"") | |
| 1516 | 1548 | if templ_7745c5c3_Err != nil { | |
| 1517 | 1549 | return templ_7745c5c3_Err | |
| 1518 | 1550 | } | |
| 1519 | 1551 | } | |
| 1520 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, ">") | |
| 1552 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, ">") | |
| 1521 | 1553 | if templ_7745c5c3_Err != nil { | |
| 1522 | 1554 | return templ_7745c5c3_Err | |
| 1523 | 1555 | } | |
| 1524 | - | var templ_7745c5c3_Var78 string | |
| 1525 | - | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed) | |
| 1556 | + | var templ_7745c5c3_Var80 string | |
| 1557 | + | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed) | |
| 1526 | 1558 | if templ_7745c5c3_Err != nil { | |
| 1527 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 291, Col: 21} | |
| 1559 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 295, Col: 21} | |
| 1528 | 1560 | } | |
| 1529 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) | |
| 1561 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var80)) | |
| 1530 | 1562 | if templ_7745c5c3_Err != nil { | |
| 1531 | 1563 | return templ_7745c5c3_Err | |
| 1532 | 1564 | } | |
| 1533 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</time></td><td class=\"c-sync\">") | |
| 1565 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</time></td><td class=\"c-sync\">") | |
| 1534 | 1566 | if templ_7745c5c3_Err != nil { | |
| 1535 | 1567 | return templ_7745c5c3_Err | |
| 1536 | 1568 | } | |
| 1537 | 1569 | if r.Synced { | |
| 1538 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "yes") | |
| 1570 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "yes") | |
| 1539 | 1571 | if templ_7745c5c3_Err != nil { | |
| 1540 | 1572 | return templ_7745c5c3_Err | |
| 1541 | 1573 | } | |
| 1542 | 1574 | } else { | |
| 1543 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "no") | |
| 1575 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "no") | |
| 1544 | 1576 | if templ_7745c5c3_Err != nil { | |
| 1545 | 1577 | return templ_7745c5c3_Err | |
| 1546 | 1578 | } | |
| 1547 | 1579 | } | |
| 1548 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</td><td class=\"c-act\"><form method=\"post\" action=\"") | |
| 1580 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</td><td class=\"c-act\"><form method=\"post\" action=\"") | |
| 1549 | 1581 | if templ_7745c5c3_Err != nil { | |
| 1550 | 1582 | return templ_7745c5c3_Err | |
| 1551 | 1583 | } | |
| 1552 | - | var templ_7745c5c3_Var79 templ.SafeURL | |
| 1553 | - | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove)) | |
| 1584 | + | var templ_7745c5c3_Var81 templ.SafeURL | |
| 1585 | + | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove)) | |
| 1554 | 1586 | if templ_7745c5c3_Err != nil { | |
| 1555 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 301, Col: 60} | |
| 1587 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 305, Col: 60} | |
| 1556 | 1588 | } | |
| 1557 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79)) | |
| 1589 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) | |
| 1558 | 1590 | if templ_7745c5c3_Err != nil { | |
| 1559 | 1591 | return templ_7745c5c3_Err | |
| 1560 | 1592 | } | |
| 1561 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"") | |
| 1593 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"") | |
| 1562 | 1594 | if templ_7745c5c3_Err != nil { | |
| 1563 | 1595 | return templ_7745c5c3_Err | |
| 1564 | 1596 | } | |
| 1565 | - | var templ_7745c5c3_Var80 string | |
| 1566 | - | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name) | |
| 1597 | + | var templ_7745c5c3_Var82 string | |
| 1598 | + | templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name) | |
| 1567 | 1599 | if templ_7745c5c3_Err != nil { | |
| 1568 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 302, Col: 89} | |
| 1600 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 306, Col: 89} | |
| 1569 | 1601 | } | |
| 1570 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80) | |
| 1602 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82) | |
| 1571 | 1603 | if templ_7745c5c3_Err != nil { | |
| 1572 | 1604 | return templ_7745c5c3_Err | |
| 1573 | 1605 | } | |
| 1574 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "\">Remove</button></form></td></tr>") | |
| 1606 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\">Remove</button></form></td></tr>") | |
| 1575 | 1607 | if templ_7745c5c3_Err != nil { | |
| 1576 | 1608 | return templ_7745c5c3_Err | |
| 1577 | 1609 | } | |
| 1578 | 1610 | } | |
| 1579 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</tbody></table>") | |
| 1611 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</tbody></table>") | |
| 1580 | 1612 | if templ_7745c5c3_Err != nil { | |
| 1581 | 1613 | return templ_7745c5c3_Err | |
| 1582 | 1614 | } | |
| 1583 | 1615 | } else { | |
| 1584 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<p class=\"lede dim\">no passkeys yet.</p>") | |
| 1616 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<p class=\"lede dim\">no passkeys yet.</p>") | |
| 1585 | 1617 | if templ_7745c5c3_Err != nil { | |
| 1586 | 1618 | return templ_7745c5c3_Err | |
| 1587 | 1619 | } | |
| 1588 | 1620 | } | |
| 1589 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, " ") | |
| 1621 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, " ") | |
| 1590 | 1622 | if templ_7745c5c3_Err != nil { | |
| 1591 | 1623 | return templ_7745c5c3_Err | |
| 1592 | 1624 | } | |
| 1593 | 1625 | if p.Full { | |
| 1594 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<p class=\"dim\">") | |
| 1626 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "<p class=\"dim\">") | |
| 1595 | 1627 | if templ_7745c5c3_Err != nil { | |
| 1596 | 1628 | return templ_7745c5c3_Err | |
| 1597 | 1629 | } | |
| 1598 | - | var templ_7745c5c3_Var81 string | |
| 1599 | - | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max)) | |
| 1630 | + | var templ_7745c5c3_Var83 string | |
| 1631 | + | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max)) | |
| 1600 | 1632 | if templ_7745c5c3_Err != nil { | |
| 1601 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 313, Col: 31} | |
| 1633 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 317, Col: 31} | |
| 1602 | 1634 | } | |
| 1603 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) | |
| 1635 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83)) | |
| 1604 | 1636 | if templ_7745c5c3_Err != nil { | |
| 1605 | 1637 | return templ_7745c5c3_Err | |
| 1606 | 1638 | } | |
| 1607 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, " passkeys are registered, the most there can be. Remove one to add another.</p>") | |
| 1639 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, " passkeys are registered, the most there can be. Remove one to add another.</p>") | |
| 1608 | 1640 | if templ_7745c5c3_Err != nil { | |
| 1609 | 1641 | return templ_7745c5c3_Err | |
| 1610 | 1642 | } | |
| 1611 | 1643 | } else { | |
| 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=\"") | |
| 1644 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<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=\"") | |
| 1613 | 1645 | if templ_7745c5c3_Err != nil { | |
| 1614 | 1646 | return templ_7745c5c3_Err | |
| 1615 | 1647 | } | |
| 1616 | - | var templ_7745c5c3_Var82 string | |
| 1617 | - | templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin) | |
| 1648 | + | var templ_7745c5c3_Var84 string | |
| 1649 | + | templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin) | |
| 1618 | 1650 | if templ_7745c5c3_Err != nil { | |
| 1619 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 318, Col: 90} | |
| 1651 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 322, Col: 90} | |
| 1620 | 1652 | } | |
| 1621 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82) | |
| 1653 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84) | |
| 1622 | 1654 | if templ_7745c5c3_Err != nil { | |
| 1623 | 1655 | return templ_7745c5c3_Err | |
| 1624 | 1656 | } | |
| 1625 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "\" data-finish=\"") | |
| 1657 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "\" data-finish=\"") | |
| 1626 | 1658 | if templ_7745c5c3_Err != nil { | |
| 1627 | 1659 | return templ_7745c5c3_Err | |
| 1628 | 1660 | } | |
| 1629 | - | var templ_7745c5c3_Var83 string | |
| 1630 | - | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish) | |
| 1661 | + | var templ_7745c5c3_Var85 string | |
| 1662 | + | templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish) | |
| 1631 | 1663 | if templ_7745c5c3_Err != nil { | |
| 1632 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 318, Col: 115} | |
| 1664 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 322, Col: 115} | |
| 1633 | 1665 | } | |
| 1634 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83) | |
| 1666 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var85) | |
| 1635 | 1667 | if templ_7745c5c3_Err != nil { | |
| 1636 | 1668 | return templ_7745c5c3_Err | |
| 1637 | 1669 | } | |
| 1638 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "\">Add a passkey</button></form>") | |
| 1670 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "\">Add a passkey</button></form>") | |
| 1639 | 1671 | if templ_7745c5c3_Err != nil { | |
| 1640 | 1672 | return templ_7745c5c3_Err | |
| 1641 | 1673 | } | |
| 1642 | 1674 | } | |
| 1643 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>") | |
| 1675 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>") | |
| 1644 | 1676 | if templ_7745c5c3_Err != nil { | |
| 1645 | 1677 | return templ_7745c5c3_Err | |
| 1646 | 1678 | } | |
| 1647 | 1679 | return nil | |
| 1648 | 1680 | }) | |
| 1649 | - | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var72), templ_7745c5c3_Buffer) | |
| 1681 | + | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var74), templ_7745c5c3_Buffer) | |
| 1650 | 1682 | if templ_7745c5c3_Err != nil { | |
| 1651 | 1683 | return templ_7745c5c3_Err | |
| 1652 | 1684 | } | |
| @@ -224,7 +224,7 @@ func Summary(l Layout, p SummaryPage) templ.Component { | |||||
| 224 | } | 224 | } | ||
| 225 | ctx = templ.InitializeContext(ctx) | 225 | ctx = templ.InitializeContext(ctx) | ||
| 226 | if p.Readme != nil { | 226 | if p.Readme != nil { | ||
| 227 | - | templ_7745c5c3_Err = ReadmeCard(*p.Readme).Render(ctx, templ_7745c5c3_Buffer) | 227 | + | templ_7745c5c3_Err = ReadmeCard(*p.Readme, p.Docs...).Render(ctx, templ_7745c5c3_Buffer) |
| 228 | if templ_7745c5c3_Err != nil { | 228 | if templ_7745c5c3_Err != nil { | ||
| 229 | return templ_7745c5c3_Err | 229 | return templ_7745c5c3_Err | ||
| 230 | } | 230 | } | ||
| @@ -528,21 +528,15 @@ func summaryRail(p SummaryPage) templ.Component { | |||||
| 528 | return templ_7745c5c3_Err | 528 | return templ_7745c5c3_Err | ||
| 529 | } | 529 | } | ||
| 530 | } | 530 | } | ||
| 531 | - | if p.Latest != nil { | 531 | + | if lic := p.License; lic != nil { |
| 532 | - | templ_7745c5c3_Err = latestTagFact(*p.Latest).Render(ctx, templ_7745c5c3_Buffer) | 532 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<dt>license</dt><dd><a href=\"") |
| 533 | - | if templ_7745c5c3_Err != nil { | |||
| 534 | - | return templ_7745c5c3_Err | |||
| 535 | - | } | |||
| 536 | - | } | |||
| 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 { | 533 | if templ_7745c5c3_Err != nil { | ||
| 540 | return templ_7745c5c3_Err | 534 | return templ_7745c5c3_Err | ||
| 541 | } | 535 | } | ||
| 542 | var templ_7745c5c3_Var27 templ.SafeURL | 536 | var templ_7745c5c3_Var27 templ.SafeURL | ||
| 543 | - | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | 537 | + | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(lic.Href)) |
| 544 | if templ_7745c5c3_Err != nil { | 538 | if templ_7745c5c3_Err != nil { | ||
| 545 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 36} | 539 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 117, Col: 41} |
| 546 | } | 540 | } | ||
| 547 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | 541 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | ||
| 548 | if templ_7745c5c3_Err != nil { | 542 | if templ_7745c5c3_Err != nil { | ||
| @@ -553,116 +547,154 @@ func summaryRail(p SummaryPage) templ.Component { | |||||
| 553 | return templ_7745c5c3_Err | 547 | return templ_7745c5c3_Err | ||
| 554 | } | 548 | } | ||
| 555 | var templ_7745c5c3_Var28 string | 549 | var templ_7745c5c3_Var28 string | ||
| 556 | - | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(r.Tag) | 550 | + | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(lic.ID) |
| 557 | if templ_7745c5c3_Err != nil { | 551 | if templ_7745c5c3_Err != nil { | ||
| 558 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 46} | 552 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 117, Col: 52} |
| 559 | } | 553 | } | ||
| 560 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) | 554 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) | ||
| 561 | if templ_7745c5c3_Err != nil { | 555 | if templ_7745c5c3_Err != nil { | ||
| 562 | return templ_7745c5c3_Err | 556 | return templ_7745c5c3_Err | ||
| 563 | } | 557 | } | ||
| 564 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</a>") | 558 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</a></dd>") |
| 559 | + | if templ_7745c5c3_Err != nil { | |||
| 560 | + | return templ_7745c5c3_Err | |||
| 561 | + | } | |||
| 562 | + | } | |||
| 563 | + | if p.Latest != nil { | |||
| 564 | + | templ_7745c5c3_Err = latestTagFact(*p.Latest).Render(ctx, templ_7745c5c3_Buffer) | |||
| 565 | + | if templ_7745c5c3_Err != nil { | |||
| 566 | + | return templ_7745c5c3_Err | |||
| 567 | + | } | |||
| 568 | + | } | |||
| 569 | + | if r := p.Release; r != nil { | |||
| 570 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<dt>release</dt><dd><a href=\"") | |||
| 565 | if templ_7745c5c3_Err != nil { | 571 | if templ_7745c5c3_Err != nil { | ||
| 566 | return templ_7745c5c3_Err | 572 | return templ_7745c5c3_Err | ||
| 567 | } | 573 | } | ||
| 568 | - | var templ_7745c5c3_Var29 string | 574 | + | var templ_7745c5c3_Var29 templ.SafeURL |
| 569 | - | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | 575 | + | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) |
| 570 | if templ_7745c5c3_Err != nil { | 576 | if templ_7745c5c3_Err != nil { | ||
| 571 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 121, Col: 60} | 577 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 125, Col: 36} |
| 572 | } | 578 | } | ||
| 573 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | 579 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | ||
| 574 | if templ_7745c5c3_Err != nil { | 580 | if templ_7745c5c3_Err != nil { | ||
| 575 | return templ_7745c5c3_Err | 581 | return templ_7745c5c3_Err | ||
| 576 | } | 582 | } | ||
| 577 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " <time datetime=\"") | 583 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">") |
| 578 | if templ_7745c5c3_Err != nil { | 584 | if templ_7745c5c3_Err != nil { | ||
| 579 | return templ_7745c5c3_Err | 585 | return templ_7745c5c3_Err | ||
| 580 | } | 586 | } | ||
| 581 | var templ_7745c5c3_Var30 string | 587 | var templ_7745c5c3_Var30 string | ||
| 582 | - | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | 588 | + | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(r.Tag) |
| 583 | if templ_7745c5c3_Err != nil { | 589 | if templ_7745c5c3_Err != nil { | ||
| 584 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 30} | 590 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 125, Col: 46} |
| 585 | } | 591 | } | ||
| 586 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30) | 592 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) |
| 587 | if templ_7745c5c3_Err != nil { | 593 | if templ_7745c5c3_Err != nil { | ||
| 588 | return templ_7745c5c3_Err | 594 | return templ_7745c5c3_Err | ||
| 589 | } | 595 | } | ||
| 590 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" title=\"") | 596 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</a>") |
| 591 | if templ_7745c5c3_Err != nil { | 597 | if templ_7745c5c3_Err != nil { | ||
| 592 | return templ_7745c5c3_Err | 598 | return templ_7745c5c3_Err | ||
| 593 | } | 599 | } | ||
| 594 | var templ_7745c5c3_Var31 string | 600 | var templ_7745c5c3_Var31 string | ||
| 595 | - | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | 601 | + | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") |
| 596 | if templ_7745c5c3_Err != nil { | 602 | if templ_7745c5c3_Err != nil { | ||
| 597 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 49} | 603 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 125, Col: 60} |
| 598 | } | 604 | } | ||
| 599 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31) | 605 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) |
| 600 | if templ_7745c5c3_Err != nil { | 606 | if templ_7745c5c3_Err != nil { | ||
| 601 | return templ_7745c5c3_Err | 607 | return templ_7745c5c3_Err | ||
| 602 | } | 608 | } | ||
| 603 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">") | 609 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " <time datetime=\"") |
| 604 | if templ_7745c5c3_Err != nil { | 610 | if templ_7745c5c3_Err != nil { | ||
| 605 | return templ_7745c5c3_Err | 611 | return templ_7745c5c3_Err | ||
| 606 | } | 612 | } | ||
| 607 | var templ_7745c5c3_Var32 string | 613 | var templ_7745c5c3_Var32 string | ||
| 608 | - | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(r.When) | 614 | + | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) |
| 609 | if templ_7745c5c3_Err != nil { | 615 | if templ_7745c5c3_Err != nil { | ||
| 610 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 60} | 616 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 126, Col: 30} |
| 611 | } | 617 | } | ||
| 612 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | 618 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) |
| 613 | if templ_7745c5c3_Err != nil { | 619 | if templ_7745c5c3_Err != nil { | ||
| 614 | return templ_7745c5c3_Err | 620 | return templ_7745c5c3_Err | ||
| 615 | } | 621 | } | ||
| 616 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</time>") | 622 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" title=\"") |
| 617 | if templ_7745c5c3_Err != nil { | 623 | if templ_7745c5c3_Err != nil { | ||
| 618 | return templ_7745c5c3_Err | 624 | return templ_7745c5c3_Err | ||
| 619 | } | 625 | } | ||
| 620 | var templ_7745c5c3_Var33 string | 626 | var templ_7745c5c3_Var33 string | ||
| 621 | - | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | 627 | + | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) |
| 622 | if templ_7745c5c3_Err != nil { | 628 | if templ_7745c5c3_Err != nil { | ||
| 623 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 122, Col: 77} | 629 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 126, Col: 49} |
| 624 | } | 630 | } | ||
| 625 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | 631 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) |
| 626 | if templ_7745c5c3_Err != nil { | 632 | if templ_7745c5c3_Err != nil { | ||
| 627 | return templ_7745c5c3_Err | 633 | return templ_7745c5c3_Err | ||
| 628 | } | 634 | } | ||
| 629 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " <a href=\"") | 635 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">") |
| 630 | if templ_7745c5c3_Err != nil { | 636 | if templ_7745c5c3_Err != nil { | ||
| 631 | return templ_7745c5c3_Err | 637 | return templ_7745c5c3_Err | ||
| 632 | } | 638 | } | ||
| 633 | - | var templ_7745c5c3_Var34 templ.SafeURL | 639 | + | var templ_7745c5c3_Var34 string |
| 634 | - | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.AllHref)) | 640 | + | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(r.When) |
| 635 | if templ_7745c5c3_Err != nil { | 641 | if templ_7745c5c3_Err != nil { | ||
| 636 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 123, Col: 39} | 642 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 126, Col: 60} |
| 637 | } | 643 | } | ||
| 638 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | 644 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | ||
| 639 | if templ_7745c5c3_Err != nil { | 645 | if templ_7745c5c3_Err != nil { | ||
| 640 | return templ_7745c5c3_Err | 646 | return templ_7745c5c3_Err | ||
| 641 | } | 647 | } | ||
| 642 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">all releases</a></dd>") | 648 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</time>") |
| 649 | + | if templ_7745c5c3_Err != nil { | |||
| 650 | + | return templ_7745c5c3_Err | |||
| 651 | + | } | |||
| 652 | + | var templ_7745c5c3_Var35 string | |||
| 653 | + | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |||
| 654 | + | if templ_7745c5c3_Err != nil { | |||
| 655 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 126, Col: 77} | |||
| 656 | + | } | |||
| 657 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |||
| 658 | + | if templ_7745c5c3_Err != nil { | |||
| 659 | + | return templ_7745c5c3_Err | |||
| 660 | + | } | |||
| 661 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " <a href=\"") | |||
| 662 | + | if templ_7745c5c3_Err != nil { | |||
| 663 | + | return templ_7745c5c3_Err | |||
| 664 | + | } | |||
| 665 | + | var templ_7745c5c3_Var36 templ.SafeURL | |||
| 666 | + | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.AllHref)) | |||
| 667 | + | if templ_7745c5c3_Err != nil { | |||
| 668 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 127, Col: 39} | |||
| 669 | + | } | |||
| 670 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | |||
| 671 | + | if templ_7745c5c3_Err != nil { | |||
| 672 | + | return templ_7745c5c3_Err | |||
| 673 | + | } | |||
| 674 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">all releases</a></dd>") | |||
| 643 | if templ_7745c5c3_Err != nil { | 675 | if templ_7745c5c3_Err != nil { | ||
| 644 | return templ_7745c5c3_Err | 676 | return templ_7745c5c3_Err | ||
| 645 | } | 677 | } | ||
| 646 | } | 678 | } | ||
| 647 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</dl></section>") | 679 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</dl></section>") |
| 648 | if templ_7745c5c3_Err != nil { | 680 | if templ_7745c5c3_Err != nil { | ||
| 649 | return templ_7745c5c3_Err | 681 | return templ_7745c5c3_Err | ||
| 650 | } | 682 | } | ||
| 651 | if len(p.Recent) > 0 { | 683 | if len(p.Recent) > 0 { | ||
| 652 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"") | 684 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"") |
| 653 | if templ_7745c5c3_Err != nil { | 685 | if templ_7745c5c3_Err != nil { | ||
| 654 | return templ_7745c5c3_Err | 686 | return templ_7745c5c3_Err | ||
| 655 | } | 687 | } | ||
| 656 | - | var templ_7745c5c3_Var35 templ.SafeURL | 688 | + | var templ_7745c5c3_Var37 templ.SafeURL |
| 657 | - | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref)) | 689 | + | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref)) |
| 658 | if templ_7745c5c3_Err != nil { | 690 | if templ_7745c5c3_Err != nil { | ||
| 659 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 129, Col: 105} | 691 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 133, Col: 105} |
| 660 | } | 692 | } | ||
| 661 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | 693 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) |
| 662 | if templ_7745c5c3_Err != nil { | 694 | if templ_7745c5c3_Err != nil { | ||
| 663 | return templ_7745c5c3_Err | 695 | return templ_7745c5c3_Err | ||
| 664 | } | 696 | } | ||
| 665 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">View all</a></h2>") | 697 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">View all</a></h2>") |
| 666 | if templ_7745c5c3_Err != nil { | 698 | if templ_7745c5c3_Err != nil { | ||
| 667 | return templ_7745c5c3_Err | 699 | return templ_7745c5c3_Err | ||
| 668 | } | 700 | } | ||
| @@ -670,7 +702,7 @@ func summaryRail(p SummaryPage) templ.Component { | |||||
| 670 | if templ_7745c5c3_Err != nil { | 702 | if templ_7745c5c3_Err != nil { | ||
| 671 | return templ_7745c5c3_Err | 703 | return templ_7745c5c3_Err | ||
| 672 | } | 704 | } | ||
| 673 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</section>") | 705 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</section>") |
| 674 | if templ_7745c5c3_Err != nil { | 706 | if templ_7745c5c3_Err != nil { | ||
| 675 | return templ_7745c5c3_Err | 707 | return templ_7745c5c3_Err | ||
| 676 | } | 708 | } | ||
| @@ -682,7 +714,7 @@ func summaryRail(p SummaryPage) templ.Component { | |||||
| 682 | } | 714 | } | ||
| 683 | } | 715 | } | ||
| 684 | if len(p.Outline) > 0 && p.Readme != nil { | 716 | if len(p.Outline) > 0 && p.Readme != nil { | ||
| 685 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>") | 717 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>") |
| 686 | if templ_7745c5c3_Err != nil { | 718 | if templ_7745c5c3_Err != nil { | ||
| 687 | return templ_7745c5c3_Err | 719 | return templ_7745c5c3_Err | ||
| 688 | } | 720 | } | ||
| @@ -690,7 +722,7 @@ func summaryRail(p SummaryPage) templ.Component { | |||||
| 690 | if templ_7745c5c3_Err != nil { | 722 | if templ_7745c5c3_Err != nil { | ||
| 691 | return templ_7745c5c3_Err | 723 | return templ_7745c5c3_Err | ||
| 692 | } | 724 | } | ||
| 693 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</section>") | 725 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</section>") |
| 694 | if templ_7745c5c3_Err != nil { | 726 | if templ_7745c5c3_Err != nil { | ||
| 695 | return templ_7745c5c3_Err | 727 | return templ_7745c5c3_Err | ||
| 696 | } | 728 | } | ||
| @@ -716,12 +748,12 @@ func Refs(l Layout, p RefsPage) templ.Component { | |||||
| 716 | }() | 748 | }() | ||
| 717 | } | 749 | } | ||
| 718 | ctx = templ.InitializeContext(ctx) | 750 | ctx = templ.InitializeContext(ctx) | ||
| 719 | - | templ_7745c5c3_Var36 := templ.GetChildren(ctx) | 751 | + | templ_7745c5c3_Var38 := templ.GetChildren(ctx) |
| 720 | - | if templ_7745c5c3_Var36 == nil { | 752 | + | if templ_7745c5c3_Var38 == nil { |
| 721 | - | templ_7745c5c3_Var36 = templ.NopComponent | 753 | + | templ_7745c5c3_Var38 = templ.NopComponent |
| 722 | } | 754 | } | ||
| 723 | ctx = templ.ClearChildren(ctx) | 755 | ctx = templ.ClearChildren(ctx) | ||
| 724 | - | templ_7745c5c3_Var37 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | 756 | + | templ_7745c5c3_Var39 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { |
| 725 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | 757 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | ||
| 726 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | 758 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | ||
| 727 | if !templ_7745c5c3_IsBuffer { | 759 | if !templ_7745c5c3_IsBuffer { | ||
| @@ -733,7 +765,7 @@ func Refs(l Layout, p RefsPage) templ.Component { | |||||
| 733 | }() | 765 | }() | ||
| 734 | } | 766 | } | ||
| 735 | ctx = templ.InitializeContext(ctx) | 767 | ctx = templ.InitializeContext(ctx) | ||
| 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>") | 768 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<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>") |
| 737 | if templ_7745c5c3_Err != nil { | 769 | if templ_7745c5c3_Err != nil { | ||
| 738 | return templ_7745c5c3_Err | 770 | return templ_7745c5c3_Err | ||
| 739 | } | 771 | } | ||
| @@ -741,32 +773,32 @@ func Refs(l Layout, p RefsPage) templ.Component { | |||||
| 741 | if templ_7745c5c3_Err != nil { | 773 | if templ_7745c5c3_Err != nil { | ||
| 742 | return templ_7745c5c3_Err | 774 | return templ_7745c5c3_Err | ||
| 743 | } | 775 | } | ||
| 744 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</table>") | 776 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</table>") |
| 745 | if templ_7745c5c3_Err != nil { | 777 | if templ_7745c5c3_Err != nil { | ||
| 746 | return templ_7745c5c3_Err | 778 | return templ_7745c5c3_Err | ||
| 747 | } | 779 | } | ||
| 748 | if p.Note != "" { | 780 | if p.Note != "" { | ||
| 749 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<p class=\"note\">") | 781 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<p class=\"note\">") |
| 750 | if templ_7745c5c3_Err != nil { | 782 | if templ_7745c5c3_Err != nil { | ||
| 751 | return templ_7745c5c3_Err | 783 | return templ_7745c5c3_Err | ||
| 752 | } | 784 | } | ||
| 753 | - | var templ_7745c5c3_Var38 string | 785 | + | var templ_7745c5c3_Var40 string |
| 754 | - | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | 786 | + | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) |
| 755 | if templ_7745c5c3_Err != nil { | 787 | if templ_7745c5c3_Err != nil { | ||
| 756 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 162, Col: 27} | 788 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 166, Col: 27} |
| 757 | } | 789 | } | ||
| 758 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) | 790 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) |
| 759 | if templ_7745c5c3_Err != nil { | 791 | if templ_7745c5c3_Err != nil { | ||
| 760 | return templ_7745c5c3_Err | 792 | return templ_7745c5c3_Err | ||
| 761 | } | 793 | } | ||
| 762 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</p>") | 794 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</p>") |
| 763 | if templ_7745c5c3_Err != nil { | 795 | if templ_7745c5c3_Err != nil { | ||
| 764 | return templ_7745c5c3_Err | 796 | return templ_7745c5c3_Err | ||
| 765 | } | 797 | } | ||
| 766 | } | 798 | } | ||
| 767 | return nil | 799 | return nil | ||
| 768 | }) | 800 | }) | ||
| 769 | - | templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var37), templ_7745c5c3_Buffer) | 801 | + | templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var39), templ_7745c5c3_Buffer) |
| 770 | if templ_7745c5c3_Err != nil { | 802 | if templ_7745c5c3_Err != nil { | ||
| 771 | return templ_7745c5c3_Err | 803 | return templ_7745c5c3_Err | ||
| 772 | } | 804 | } | ||
| @@ -790,112 +822,112 @@ func refRow(kind string, r RefRow) templ.Component { | |||||
| 790 | }() | 822 | }() | ||
| 791 | } | 823 | } | ||
| 792 | ctx = templ.InitializeContext(ctx) | 824 | ctx = templ.InitializeContext(ctx) | ||
| 793 | - | templ_7745c5c3_Var39 := templ.GetChildren(ctx) | 825 | + | templ_7745c5c3_Var41 := templ.GetChildren(ctx) |
| 794 | - | if templ_7745c5c3_Var39 == nil { | 826 | + | if templ_7745c5c3_Var41 == nil { |
| 795 | - | templ_7745c5c3_Var39 = templ.NopComponent | 827 | + | templ_7745c5c3_Var41 = templ.NopComponent |
| 796 | } | 828 | } | ||
| 797 | ctx = templ.ClearChildren(ctx) | 829 | ctx = templ.ClearChildren(ctx) | ||
| 798 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<tr") | 830 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<tr") |
| 799 | if templ_7745c5c3_Err != nil { | 831 | if templ_7745c5c3_Err != nil { | ||
| 800 | return templ_7745c5c3_Err | 832 | return templ_7745c5c3_Err | ||
| 801 | } | 833 | } | ||
| 802 | if r.Cursor { | 834 | if r.Cursor { | ||
| 803 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " data-cursor") | 835 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " data-cursor") |
| 804 | if templ_7745c5c3_Err != nil { | 836 | if templ_7745c5c3_Err != nil { | ||
| 805 | return templ_7745c5c3_Err | 837 | return templ_7745c5c3_Err | ||
| 806 | } | 838 | } | ||
| 807 | } | 839 | } | ||
| 808 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "><td class=\"c-kind mute\">") | 840 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "><td class=\"c-kind mute\">") |
| 809 | if templ_7745c5c3_Err != nil { | 841 | if templ_7745c5c3_Err != nil { | ||
| 810 | return templ_7745c5c3_Err | 842 | return templ_7745c5c3_Err | ||
| 811 | } | 843 | } | ||
| 812 | - | var templ_7745c5c3_Var40 string | 844 | + | var templ_7745c5c3_Var42 string |
| 813 | - | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(kind) | 845 | + | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(kind) |
| 814 | if templ_7745c5c3_Err != nil { | 846 | if templ_7745c5c3_Err != nil { | ||
| 815 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 169, Col: 32} | 847 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 173, Col: 32} |
| 816 | } | 848 | } | ||
| 817 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | 849 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) |
| 818 | if templ_7745c5c3_Err != nil { | 850 | if templ_7745c5c3_Err != nil { | ||
| 819 | return templ_7745c5c3_Err | 851 | return templ_7745c5c3_Err | ||
| 820 | } | 852 | } | ||
| 821 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</td><td class=\"c-name\"><a href=\"") | 853 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</td><td class=\"c-name\"><a href=\"") |
| 822 | if templ_7745c5c3_Err != nil { | 854 | if templ_7745c5c3_Err != nil { | ||
| 823 | return templ_7745c5c3_Err | 855 | return templ_7745c5c3_Err | ||
| 824 | } | 856 | } | ||
| 825 | - | var templ_7745c5c3_Var41 templ.SafeURL | 857 | + | var templ_7745c5c3_Var43 templ.SafeURL |
| 826 | - | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) | 858 | + | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href)) |
| 827 | if templ_7745c5c3_Err != nil { | 859 | if templ_7745c5c3_Err != nil { | ||
| 828 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 171, Col: 34} | 860 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 175, Col: 34} |
| 829 | } | 861 | } | ||
| 830 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | 862 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) |
| 831 | if templ_7745c5c3_Err != nil { | 863 | if templ_7745c5c3_Err != nil { | ||
| 832 | return templ_7745c5c3_Err | 864 | return templ_7745c5c3_Err | ||
| 833 | } | 865 | } | ||
| 834 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">") | 866 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">") |
| 835 | if templ_7745c5c3_Err != nil { | 867 | if templ_7745c5c3_Err != nil { | ||
| 836 | return templ_7745c5c3_Err | 868 | return templ_7745c5c3_Err | ||
| 837 | } | 869 | } | ||
| 838 | - | var templ_7745c5c3_Var42 string | 870 | + | var templ_7745c5c3_Var44 string |
| 839 | - | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | 871 | + | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) |
| 840 | if templ_7745c5c3_Err != nil { | 872 | if templ_7745c5c3_Err != nil { | ||
| 841 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 171, Col: 45} | 873 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 175, Col: 45} |
| 842 | } | 874 | } | ||
| 843 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | 875 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) |
| 844 | if templ_7745c5c3_Err != nil { | 876 | if templ_7745c5c3_Err != nil { | ||
| 845 | return templ_7745c5c3_Err | 877 | return templ_7745c5c3_Err | ||
| 846 | } | 878 | } | ||
| 847 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</a> ") | 879 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</a> ") |
| 848 | if templ_7745c5c3_Err != nil { | 880 | if templ_7745c5c3_Err != nil { | ||
| 849 | return templ_7745c5c3_Err | 881 | return templ_7745c5c3_Err | ||
| 850 | } | 882 | } | ||
| 851 | if r.Default { | 883 | if r.Default { | ||
| 852 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<span class=\"pill\">default</span>") | 884 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<span class=\"pill\">default</span>") |
| 853 | if templ_7745c5c3_Err != nil { | 885 | if templ_7745c5c3_Err != nil { | ||
| 854 | return templ_7745c5c3_Err | 886 | return templ_7745c5c3_Err | ||
| 855 | } | 887 | } | ||
| 856 | } | 888 | } | ||
| 857 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</td><td class=\"c-sha\">") | 889 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</td><td class=\"c-sha\">") |
| 858 | if templ_7745c5c3_Err != nil { | 890 | if templ_7745c5c3_Err != nil { | ||
| 859 | return templ_7745c5c3_Err | 891 | return templ_7745c5c3_Err | ||
| 860 | } | 892 | } | ||
| 861 | if r.CHref != "" { | 893 | if r.CHref != "" { | ||
| 862 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a href=\"") | 894 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<a href=\"") |
| 863 | if templ_7745c5c3_Err != nil { | 895 | if templ_7745c5c3_Err != nil { | ||
| 864 | return templ_7745c5c3_Err | 896 | return templ_7745c5c3_Err | ||
| 865 | } | 897 | } | ||
| 866 | - | var templ_7745c5c3_Var43 templ.SafeURL | 898 | + | var templ_7745c5c3_Var45 templ.SafeURL |
| 867 | - | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref)) | 899 | + | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref)) |
| 868 | if templ_7745c5c3_Err != nil { | 900 | if templ_7745c5c3_Err != nil { | ||
| 869 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 178, Col: 36} | 901 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 182, Col: 36} |
| 870 | } | 902 | } | ||
| 871 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | 903 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) |
| 872 | if templ_7745c5c3_Err != nil { | 904 | if templ_7745c5c3_Err != nil { | ||
| 873 | return templ_7745c5c3_Err | 905 | return templ_7745c5c3_Err | ||
| 874 | } | 906 | } | ||
| 875 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\">") | 907 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\">") |
| 876 | if templ_7745c5c3_Err != nil { | 908 | if templ_7745c5c3_Err != nil { | ||
| 877 | return templ_7745c5c3_Err | 909 | return templ_7745c5c3_Err | ||
| 878 | } | 910 | } | ||
| 879 | - | var templ_7745c5c3_Var44 string | 911 | + | var templ_7745c5c3_Var46 string |
| 880 | - | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short) | 912 | + | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short) |
| 881 | if templ_7745c5c3_Err != nil { | 913 | if templ_7745c5c3_Err != nil { | ||
| 882 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 178, Col: 48} | 914 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 182, Col: 48} |
| 883 | } | 915 | } | ||
| 884 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | 916 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) |
| 885 | if templ_7745c5c3_Err != nil { | 917 | if templ_7745c5c3_Err != nil { | ||
| 886 | return templ_7745c5c3_Err | 918 | return templ_7745c5c3_Err | ||
| 887 | } | 919 | } | ||
| 888 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</a>") | 920 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</a>") |
| 889 | if templ_7745c5c3_Err != nil { | 921 | if templ_7745c5c3_Err != nil { | ||
| 890 | return templ_7745c5c3_Err | 922 | return templ_7745c5c3_Err | ||
| 891 | } | 923 | } | ||
| 892 | } else { | 924 | } else { | ||
| 893 | - | var templ_7745c5c3_Var45 string | 925 | + | var templ_7745c5c3_Var47 string |
| 894 | - | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short) | 926 | + | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short) |
| 895 | if templ_7745c5c3_Err != nil { | 927 | if templ_7745c5c3_Err != nil { | ||
| 896 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 180, Col: 13} | 928 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 184, Col: 13} |
| 897 | } | 929 | } | ||
| 898 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | 930 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) |
| 899 | if templ_7745c5c3_Err != nil { | 931 | if templ_7745c5c3_Err != nil { | ||
| 900 | return templ_7745c5c3_Err | 932 | return templ_7745c5c3_Err | ||
| 901 | } | 933 | } | ||
| @@ -904,69 +936,69 @@ func refRow(kind string, r RefRow) templ.Component { | |||||
| 904 | if templ_7745c5c3_Err != nil { | 936 | if templ_7745c5c3_Err != nil { | ||
| 905 | return templ_7745c5c3_Err | 937 | return templ_7745c5c3_Err | ||
| 906 | } | 938 | } | ||
| 907 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</td><td class=\"c-sub\">") | 939 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</td><td class=\"c-sub\">") |
| 908 | if templ_7745c5c3_Err != nil { | 940 | if templ_7745c5c3_Err != nil { | ||
| 909 | return templ_7745c5c3_Err | 941 | return templ_7745c5c3_Err | ||
| 910 | } | 942 | } | ||
| 911 | - | var templ_7745c5c3_Var46 string | 943 | + | var templ_7745c5c3_Var48 string |
| 912 | - | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject) | 944 | + | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject) |
| 913 | if templ_7745c5c3_Err != nil { | 945 | if templ_7745c5c3_Err != nil { | ||
| 914 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 184, Col: 31} | 946 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 188, Col: 31} |
| 915 | } | 947 | } | ||
| 916 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | 948 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) |
| 917 | if templ_7745c5c3_Err != nil { | 949 | if templ_7745c5c3_Err != nil { | ||
| 918 | return templ_7745c5c3_Err | 950 | return templ_7745c5c3_Err | ||
| 919 | } | 951 | } | ||
| 920 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</td><td class=\"c-when dim\">") | 952 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</td><td class=\"c-when dim\">") |
| 921 | if templ_7745c5c3_Err != nil { | 953 | if templ_7745c5c3_Err != nil { | ||
| 922 | return templ_7745c5c3_Err | 954 | return templ_7745c5c3_Err | ||
| 923 | } | 955 | } | ||
| 924 | if r.WhenAt != "" { | 956 | if r.WhenAt != "" { | ||
| 925 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<time datetime=\"") | 957 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<time datetime=\"") |
| 926 | if templ_7745c5c3_Err != nil { | 958 | if templ_7745c5c3_Err != nil { | ||
| 927 | return templ_7745c5c3_Err | 959 | return templ_7745c5c3_Err | ||
| 928 | } | 960 | } | ||
| 929 | - | var templ_7745c5c3_Var47 string | 961 | + | var templ_7745c5c3_Var49 string |
| 930 | - | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | 962 | + | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) |
| 931 | if templ_7745c5c3_Err != nil { | 963 | if templ_7745c5c3_Err != nil { | ||
| 932 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 29} | 964 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 191, Col: 29} |
| 933 | } | 965 | } | ||
| 934 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47) | 966 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49) |
| 935 | if templ_7745c5c3_Err != nil { | 967 | if templ_7745c5c3_Err != nil { | ||
| 936 | return templ_7745c5c3_Err | 968 | return templ_7745c5c3_Err | ||
| 937 | } | 969 | } | ||
| 938 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" title=\"") | 970 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\" title=\"") |
| 939 | if templ_7745c5c3_Err != nil { | 971 | if templ_7745c5c3_Err != nil { | ||
| 940 | return templ_7745c5c3_Err | 972 | return templ_7745c5c3_Err | ||
| 941 | } | 973 | } | ||
| 942 | - | var templ_7745c5c3_Var48 string | 974 | + | var templ_7745c5c3_Var50 string |
| 943 | - | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) | 975 | + | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt) |
| 944 | if templ_7745c5c3_Err != nil { | 976 | if templ_7745c5c3_Err != nil { | ||
| 945 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 48} | 977 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 191, Col: 48} |
| 946 | } | 978 | } | ||
| 947 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48) | 979 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50) |
| 948 | if templ_7745c5c3_Err != nil { | 980 | if templ_7745c5c3_Err != nil { | ||
| 949 | return templ_7745c5c3_Err | 981 | return templ_7745c5c3_Err | ||
| 950 | } | 982 | } | ||
| 951 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\">") | 983 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\">") |
| 952 | if templ_7745c5c3_Err != nil { | 984 | if templ_7745c5c3_Err != nil { | ||
| 953 | return templ_7745c5c3_Err | 985 | return templ_7745c5c3_Err | ||
| 954 | } | 986 | } | ||
| 955 | - | var templ_7745c5c3_Var49 string | 987 | + | var templ_7745c5c3_Var51 string |
| 956 | - | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(r.When) | 988 | + | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(r.When) |
| 957 | if templ_7745c5c3_Err != nil { | 989 | if templ_7745c5c3_Err != nil { | ||
| 958 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 187, Col: 59} | 990 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 191, Col: 59} |
| 959 | } | 991 | } | ||
| 960 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) | 992 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) |
| 961 | if templ_7745c5c3_Err != nil { | 993 | if templ_7745c5c3_Err != nil { | ||
| 962 | return templ_7745c5c3_Err | 994 | return templ_7745c5c3_Err | ||
| 963 | } | 995 | } | ||
| 964 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</time>") | 996 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</time>") |
| 965 | if templ_7745c5c3_Err != nil { | 997 | if templ_7745c5c3_Err != nil { | ||
| 966 | return templ_7745c5c3_Err | 998 | return templ_7745c5c3_Err | ||
| 967 | } | 999 | } | ||
| 968 | } | 1000 | } | ||
| 969 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</td></tr>") | 1001 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</td></tr>") |
| 970 | if templ_7745c5c3_Err != nil { | 1002 | if templ_7745c5c3_Err != nil { | ||
| 971 | return templ_7745c5c3_Err | 1003 | return templ_7745c5c3_Err | ||
| 972 | } | 1004 | } | ||
| @@ -990,12 +1022,12 @@ func refsRail(p RefsPage) templ.Component { | |||||
| 990 | }() | 1022 | }() | ||
| 991 | } | 1023 | } | ||
| 992 | ctx = templ.InitializeContext(ctx) | 1024 | ctx = templ.InitializeContext(ctx) | ||
| 993 | - | templ_7745c5c3_Var50 := templ.GetChildren(ctx) | 1025 | + | templ_7745c5c3_Var52 := templ.GetChildren(ctx) |
| 994 | - | if templ_7745c5c3_Var50 == nil { | 1026 | + | if templ_7745c5c3_Var52 == nil { |
| 995 | - | templ_7745c5c3_Var50 = templ.NopComponent | 1027 | + | templ_7745c5c3_Var52 = templ.NopComponent |
| 996 | } | 1028 | } | ||
| 997 | ctx = templ.ClearChildren(ctx) | 1029 | ctx = templ.ClearChildren(ctx) | ||
| 998 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>") | 1030 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>") |
| 999 | if templ_7745c5c3_Err != nil { | 1031 | if templ_7745c5c3_Err != nil { | ||
| 1000 | return templ_7745c5c3_Err | 1032 | return templ_7745c5c3_Err | ||
| 1001 | } | 1033 | } | ||
| @@ -1003,7 +1035,7 @@ func refsRail(p RefsPage) templ.Component { | |||||
| 1003 | if templ_7745c5c3_Err != nil { | 1035 | if templ_7745c5c3_Err != nil { | ||
| 1004 | return templ_7745c5c3_Err | 1036 | return templ_7745c5c3_Err | ||
| 1005 | } | 1037 | } | ||
| 1006 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</section>") | 1038 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</section>") |
| 1007 | if templ_7745c5c3_Err != nil { | 1039 | if templ_7745c5c3_Err != nil { | ||
| 1008 | return templ_7745c5c3_Err | 1040 | return templ_7745c5c3_Err | ||
| 1009 | } | 1041 | } | ||
| @@ -1028,12 +1060,12 @@ func State(l Layout, p StatePage) templ.Component { | |||||
| 1028 | }() | 1060 | }() | ||
| 1029 | } | 1061 | } | ||
| 1030 | ctx = templ.InitializeContext(ctx) | 1062 | ctx = templ.InitializeContext(ctx) | ||
| 1031 | - | templ_7745c5c3_Var51 := templ.GetChildren(ctx) | 1063 | + | templ_7745c5c3_Var53 := templ.GetChildren(ctx) |
| 1032 | - | if templ_7745c5c3_Var51 == nil { | 1064 | + | if templ_7745c5c3_Var53 == nil { |
| 1033 | - | templ_7745c5c3_Var51 = templ.NopComponent | 1065 | + | templ_7745c5c3_Var53 = templ.NopComponent |
| 1034 | } | 1066 | } | ||
| 1035 | ctx = templ.ClearChildren(ctx) | 1067 | ctx = templ.ClearChildren(ctx) | ||
| 1036 | - | templ_7745c5c3_Var52 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | 1068 | + | templ_7745c5c3_Var54 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { |
| 1037 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | 1069 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | ||
| 1038 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | 1070 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | ||
| 1039 | if !templ_7745c5c3_IsBuffer { | 1071 | if !templ_7745c5c3_IsBuffer { | ||
| @@ -1045,51 +1077,51 @@ func State(l Layout, p StatePage) templ.Component { | |||||
| 1045 | }() | 1077 | }() | ||
| 1046 | } | 1078 | } | ||
| 1047 | ctx = templ.InitializeContext(ctx) | 1079 | ctx = templ.InitializeContext(ctx) | ||
| 1048 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">") | 1080 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">") |
| 1049 | if templ_7745c5c3_Err != nil { | 1081 | if templ_7745c5c3_Err != nil { | ||
| 1050 | return templ_7745c5c3_Err | 1082 | return templ_7745c5c3_Err | ||
| 1051 | } | 1083 | } | ||
| 1052 | - | var templ_7745c5c3_Var53 string | 1084 | + | var templ_7745c5c3_Var55 string |
| 1053 | - | templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code) | 1085 | + | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code) |
| 1054 | if templ_7745c5c3_Err != nil { | 1086 | if templ_7745c5c3_Err != nil { | ||
| 1055 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 204, Col: 30} | 1087 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 208, Col: 30} |
| 1056 | } | 1088 | } | ||
| 1057 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) | 1089 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) |
| 1058 | if templ_7745c5c3_Err != nil { | 1090 | if templ_7745c5c3_Err != nil { | ||
| 1059 | return templ_7745c5c3_Err | 1091 | return templ_7745c5c3_Err | ||
| 1060 | } | 1092 | } | ||
| 1061 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</p><h1 id=\"state-title\">") | 1093 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</p><h1 id=\"state-title\">") |
| 1062 | if templ_7745c5c3_Err != nil { | 1094 | if templ_7745c5c3_Err != nil { | ||
| 1063 | return templ_7745c5c3_Err | 1095 | return templ_7745c5c3_Err | ||
| 1064 | } | 1096 | } | ||
| 1065 | - | var templ_7745c5c3_Var54 string | 1097 | + | var templ_7745c5c3_Var56 string |
| 1066 | - | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | 1098 | + | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) |
| 1067 | if templ_7745c5c3_Err != nil { | 1099 | if templ_7745c5c3_Err != nil { | ||
| 1068 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 205, Col: 33} | 1100 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 209, Col: 33} |
| 1069 | } | 1101 | } | ||
| 1070 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | 1102 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) |
| 1071 | if templ_7745c5c3_Err != nil { | 1103 | if templ_7745c5c3_Err != nil { | ||
| 1072 | return templ_7745c5c3_Err | 1104 | return templ_7745c5c3_Err | ||
| 1073 | } | 1105 | } | ||
| 1074 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</h1>") | 1106 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</h1>") |
| 1075 | if templ_7745c5c3_Err != nil { | 1107 | if templ_7745c5c3_Err != nil { | ||
| 1076 | return templ_7745c5c3_Err | 1108 | return templ_7745c5c3_Err | ||
| 1077 | } | 1109 | } | ||
| 1078 | for _, line := range p.Lines { | 1110 | for _, line := range p.Lines { | ||
| 1079 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<p class=\"dim\">") | 1111 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<p class=\"dim\">") |
| 1080 | if templ_7745c5c3_Err != nil { | 1112 | if templ_7745c5c3_Err != nil { | ||
| 1081 | return templ_7745c5c3_Err | 1113 | return templ_7745c5c3_Err | ||
| 1082 | } | 1114 | } | ||
| 1083 | - | var templ_7745c5c3_Var55 string | 1115 | + | var templ_7745c5c3_Var57 string |
| 1084 | - | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(line) | 1116 | + | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(line) |
| 1085 | if templ_7745c5c3_Err != nil { | 1117 | if templ_7745c5c3_Err != nil { | ||
| 1086 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 207, Col: 25} | 1118 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 211, Col: 25} |
| 1087 | } | 1119 | } | ||
| 1088 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | 1120 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) |
| 1089 | if templ_7745c5c3_Err != nil { | 1121 | if templ_7745c5c3_Err != nil { | ||
| 1090 | return templ_7745c5c3_Err | 1122 | return templ_7745c5c3_Err | ||
| 1091 | } | 1123 | } | ||
| 1092 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</p>") | 1124 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</p>") |
| 1093 | if templ_7745c5c3_Err != nil { | 1125 | if templ_7745c5c3_Err != nil { | ||
| 1094 | return templ_7745c5c3_Err | 1126 | return templ_7745c5c3_Err | ||
| 1095 | } | 1127 | } | ||
| @@ -1100,49 +1132,49 @@ func State(l Layout, p StatePage) templ.Component { | |||||
| 1100 | return templ_7745c5c3_Err | 1132 | return templ_7745c5c3_Err | ||
| 1101 | } | 1133 | } | ||
| 1102 | } | 1134 | } | ||
| 1103 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<p class=\"state-back\">") | 1135 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<p class=\"state-back\">") |
| 1104 | if templ_7745c5c3_Err != nil { | 1136 | if templ_7745c5c3_Err != nil { | ||
| 1105 | return templ_7745c5c3_Err | 1137 | return templ_7745c5c3_Err | ||
| 1106 | } | 1138 | } | ||
| 1107 | for _, b := range p.Back { | 1139 | for _, b := range p.Back { | ||
| 1108 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<a href=\"") | 1140 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<a href=\"") |
| 1109 | if templ_7745c5c3_Err != nil { | 1141 | if templ_7745c5c3_Err != nil { | ||
| 1110 | return templ_7745c5c3_Err | 1142 | return templ_7745c5c3_Err | ||
| 1111 | } | 1143 | } | ||
| 1112 | - | var templ_7745c5c3_Var56 templ.SafeURL | 1144 | + | var templ_7745c5c3_Var58 templ.SafeURL |
| 1113 | - | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href)) | 1145 | + | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href)) |
| 1114 | if templ_7745c5c3_Err != nil { | 1146 | if templ_7745c5c3_Err != nil { | ||
| 1115 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 214, Col: 36} | 1147 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 218, Col: 36} |
| 1116 | } | 1148 | } | ||
| 1117 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) | 1149 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) |
| 1118 | if templ_7745c5c3_Err != nil { | 1150 | if templ_7745c5c3_Err != nil { | ||
| 1119 | return templ_7745c5c3_Err | 1151 | return templ_7745c5c3_Err | ||
| 1120 | } | 1152 | } | ||
| 1121 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "\">") | 1153 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\">") |
| 1122 | if templ_7745c5c3_Err != nil { | 1154 | if templ_7745c5c3_Err != nil { | ||
| 1123 | return templ_7745c5c3_Err | 1155 | return templ_7745c5c3_Err | ||
| 1124 | } | 1156 | } | ||
| 1125 | - | var templ_7745c5c3_Var57 string | 1157 | + | var templ_7745c5c3_Var59 string |
| 1126 | - | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label) | 1158 | + | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label) |
| 1127 | if templ_7745c5c3_Err != nil { | 1159 | if templ_7745c5c3_Err != nil { | ||
| 1128 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 214, Col: 48} | 1160 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 218, Col: 48} |
| 1129 | } | 1161 | } | ||
| 1130 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) | 1162 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) |
| 1131 | if templ_7745c5c3_Err != nil { | 1163 | if templ_7745c5c3_Err != nil { | ||
| 1132 | return templ_7745c5c3_Err | 1164 | return templ_7745c5c3_Err | ||
| 1133 | } | 1165 | } | ||
| 1134 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</a>") | 1166 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</a>") |
| 1135 | if templ_7745c5c3_Err != nil { | 1167 | if templ_7745c5c3_Err != nil { | ||
| 1136 | return templ_7745c5c3_Err | 1168 | return templ_7745c5c3_Err | ||
| 1137 | } | 1169 | } | ||
| 1138 | } | 1170 | } | ||
| 1139 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</p></section>") | 1171 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</p></section>") |
| 1140 | if templ_7745c5c3_Err != nil { | 1172 | if templ_7745c5c3_Err != nil { | ||
| 1141 | return templ_7745c5c3_Err | 1173 | return templ_7745c5c3_Err | ||
| 1142 | } | 1174 | } | ||
| 1143 | return nil | 1175 | return nil | ||
| 1144 | }) | 1176 | }) | ||
| 1145 | - | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var52), templ_7745c5c3_Buffer) | 1177 | + | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var54), templ_7745c5c3_Buffer) |
| 1146 | if templ_7745c5c3_Err != nil { | 1178 | if templ_7745c5c3_Err != nil { | ||
| 1147 | return templ_7745c5c3_Err | 1179 | return templ_7745c5c3_Err | ||
| 1148 | } | 1180 | } | ||
| @@ -1171,12 +1203,12 @@ func Login(l Layout, p LoginPage) templ.Component { | |||||
| 1171 | }() | 1203 | }() | ||
| 1172 | } | 1204 | } | ||
| 1173 | ctx = templ.InitializeContext(ctx) | 1205 | ctx = templ.InitializeContext(ctx) | ||
| 1174 | - | templ_7745c5c3_Var58 := templ.GetChildren(ctx) | 1206 | + | templ_7745c5c3_Var60 := templ.GetChildren(ctx) |
| 1175 | - | if templ_7745c5c3_Var58 == nil { | 1207 | + | if templ_7745c5c3_Var60 == nil { |
| 1176 | - | templ_7745c5c3_Var58 = templ.NopComponent | 1208 | + | templ_7745c5c3_Var60 = templ.NopComponent |
| 1177 | } | 1209 | } | ||
| 1178 | ctx = templ.ClearChildren(ctx) | 1210 | ctx = templ.ClearChildren(ctx) | ||
| 1179 | - | templ_7745c5c3_Var59 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | 1211 | + | templ_7745c5c3_Var61 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { |
| 1180 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | 1212 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | ||
| 1181 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | 1213 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | ||
| 1182 | if !templ_7745c5c3_IsBuffer { | 1214 | if !templ_7745c5c3_IsBuffer { | ||
| @@ -1188,75 +1220,75 @@ func Login(l Layout, p LoginPage) templ.Component { | |||||
| 1188 | }() | 1220 | }() | ||
| 1189 | } | 1221 | } | ||
| 1190 | ctx = templ.InitializeContext(ctx) | 1222 | ctx = templ.InitializeContext(ctx) | ||
| 1191 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">") | 1223 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">") |
| 1192 | if templ_7745c5c3_Err != nil { | 1224 | if templ_7745c5c3_Err != nil { | ||
| 1193 | return templ_7745c5c3_Err | 1225 | return templ_7745c5c3_Err | ||
| 1194 | } | 1226 | } | ||
| 1195 | - | var templ_7745c5c3_Var60 string | 1227 | + | var templ_7745c5c3_Var62 string |
| 1196 | - | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | 1228 | + | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) |
| 1197 | if templ_7745c5c3_Err != nil { | 1229 | if templ_7745c5c3_Err != nil { | ||
| 1198 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 229, Col: 33} | 1230 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 233, Col: 33} |
| 1199 | } | 1231 | } | ||
| 1200 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) | 1232 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) |
| 1201 | if templ_7745c5c3_Err != nil { | 1233 | if templ_7745c5c3_Err != nil { | ||
| 1202 | return templ_7745c5c3_Err | 1234 | return templ_7745c5c3_Err | ||
| 1203 | } | 1235 | } | ||
| 1204 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</h1>") | 1236 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</h1>") |
| 1205 | if templ_7745c5c3_Err != nil { | 1237 | if templ_7745c5c3_Err != nil { | ||
| 1206 | return templ_7745c5c3_Err | 1238 | return templ_7745c5c3_Err | ||
| 1207 | } | 1239 | } | ||
| 1208 | for _, line := range p.Lines { | 1240 | for _, line := range p.Lines { | ||
| 1209 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<p class=\"dim\">") | 1241 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<p class=\"dim\">") |
| 1210 | if templ_7745c5c3_Err != nil { | 1242 | if templ_7745c5c3_Err != nil { | ||
| 1211 | return templ_7745c5c3_Err | 1243 | return templ_7745c5c3_Err | ||
| 1212 | } | 1244 | } | ||
| 1213 | - | var templ_7745c5c3_Var61 string | 1245 | + | var templ_7745c5c3_Var63 string |
| 1214 | - | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(line) | 1246 | + | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(line) |
| 1215 | if templ_7745c5c3_Err != nil { | 1247 | if templ_7745c5c3_Err != nil { | ||
| 1216 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 231, Col: 25} | 1248 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 235, Col: 25} |
| 1217 | } | 1249 | } | ||
| 1218 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | 1250 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) |
| 1219 | if templ_7745c5c3_Err != nil { | 1251 | if templ_7745c5c3_Err != nil { | ||
| 1220 | return templ_7745c5c3_Err | 1252 | return templ_7745c5c3_Err | ||
| 1221 | } | 1253 | } | ||
| 1222 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</p>") | 1254 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</p>") |
| 1223 | if templ_7745c5c3_Err != nil { | 1255 | if templ_7745c5c3_Err != nil { | ||
| 1224 | return templ_7745c5c3_Err | 1256 | return templ_7745c5c3_Err | ||
| 1225 | } | 1257 | } | ||
| 1226 | } | 1258 | } | ||
| 1227 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "<form class=\"state-act\" method=\"post\" action=\"") | 1259 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<form class=\"state-act\" method=\"post\" action=\"") |
| 1228 | if templ_7745c5c3_Err != nil { | 1260 | if templ_7745c5c3_Err != nil { | ||
| 1229 | return templ_7745c5c3_Err | 1261 | return templ_7745c5c3_Err | ||
| 1230 | } | 1262 | } | ||
| 1231 | - | var templ_7745c5c3_Var62 templ.SafeURL | 1263 | + | var templ_7745c5c3_Var64 templ.SafeURL |
| 1232 | - | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action)) | 1264 | + | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action)) |
| 1233 | if templ_7745c5c3_Err != nil { | 1265 | if templ_7745c5c3_Err != nil { | ||
| 1234 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 233, Col: 73} | 1266 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 237, Col: 73} |
| 1235 | } | 1267 | } | ||
| 1236 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | 1268 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64)) |
| 1237 | if templ_7745c5c3_Err != nil { | 1269 | if templ_7745c5c3_Err != nil { | ||
| 1238 | return templ_7745c5c3_Err | 1270 | return templ_7745c5c3_Err | ||
| 1239 | } | 1271 | } | ||
| 1240 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">") | 1272 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">") |
| 1241 | if templ_7745c5c3_Err != nil { | 1273 | if templ_7745c5c3_Err != nil { | ||
| 1242 | return templ_7745c5c3_Err | 1274 | return templ_7745c5c3_Err | ||
| 1243 | } | 1275 | } | ||
| 1244 | - | var templ_7745c5c3_Var63 string | 1276 | + | var templ_7745c5c3_Var65 string |
| 1245 | - | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button) | 1277 | + | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button) |
| 1246 | if templ_7745c5c3_Err != nil { | 1278 | if templ_7745c5c3_Err != nil { | ||
| 1247 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 234, Col: 56} | 1279 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 238, Col: 56} |
| 1248 | } | 1280 | } | ||
| 1249 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) | 1281 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) |
| 1250 | if templ_7745c5c3_Err != nil { | 1282 | if templ_7745c5c3_Err != nil { | ||
| 1251 | return templ_7745c5c3_Err | 1283 | return templ_7745c5c3_Err | ||
| 1252 | } | 1284 | } | ||
| 1253 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>") | 1285 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>") |
| 1254 | if templ_7745c5c3_Err != nil { | 1286 | if templ_7745c5c3_Err != nil { | ||
| 1255 | return templ_7745c5c3_Err | 1287 | return templ_7745c5c3_Err | ||
| 1256 | } | 1288 | } | ||
| 1257 | return nil | 1289 | return nil | ||
| 1258 | }) | 1290 | }) | ||
| 1259 | - | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var59), templ_7745c5c3_Buffer) | 1291 | + | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var61), templ_7745c5c3_Buffer) |
| 1260 | if templ_7745c5c3_Err != nil { | 1292 | if templ_7745c5c3_Err != nil { | ||
| 1261 | return templ_7745c5c3_Err | 1293 | return templ_7745c5c3_Err | ||
| 1262 | } | 1294 | } | ||
| @@ -1282,12 +1314,12 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component { | |||||
| 1282 | }() | 1314 | }() | ||
| 1283 | } | 1315 | } | ||
| 1284 | ctx = templ.InitializeContext(ctx) | 1316 | ctx = templ.InitializeContext(ctx) | ||
| 1285 | - | templ_7745c5c3_Var64 := templ.GetChildren(ctx) | 1317 | + | templ_7745c5c3_Var66 := templ.GetChildren(ctx) |
| 1286 | - | if templ_7745c5c3_Var64 == nil { | 1318 | + | if templ_7745c5c3_Var66 == nil { |
| 1287 | - | templ_7745c5c3_Var64 = templ.NopComponent | 1319 | + | templ_7745c5c3_Var66 = templ.NopComponent |
| 1288 | } | 1320 | } | ||
| 1289 | ctx = templ.ClearChildren(ctx) | 1321 | ctx = templ.ClearChildren(ctx) | ||
| 1290 | - | templ_7745c5c3_Var65 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | 1322 | + | templ_7745c5c3_Var67 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { |
| 1291 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | 1323 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | ||
| 1292 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | 1324 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | ||
| 1293 | if !templ_7745c5c3_IsBuffer { | 1325 | if !templ_7745c5c3_IsBuffer { | ||
| @@ -1299,88 +1331,88 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component { | |||||
| 1299 | }() | 1331 | }() | ||
| 1300 | } | 1332 | } | ||
| 1301 | ctx = templ.InitializeContext(ctx) | 1333 | ctx = templ.InitializeContext(ctx) | ||
| 1302 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">") | 1334 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">") |
| 1303 | if templ_7745c5c3_Err != nil { | 1335 | if templ_7745c5c3_Err != nil { | ||
| 1304 | return templ_7745c5c3_Err | 1336 | return templ_7745c5c3_Err | ||
| 1305 | } | 1337 | } | ||
| 1306 | - | var templ_7745c5c3_Var66 string | 1338 | + | var templ_7745c5c3_Var68 string |
| 1307 | - | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | 1339 | + | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) |
| 1308 | if templ_7745c5c3_Err != nil { | 1340 | if templ_7745c5c3_Err != nil { | ||
| 1309 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 246, Col: 33} | 1341 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 250, Col: 33} |
| 1310 | } | 1342 | } | ||
| 1311 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) | 1343 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) |
| 1312 | if templ_7745c5c3_Err != nil { | 1344 | if templ_7745c5c3_Err != nil { | ||
| 1313 | return templ_7745c5c3_Err | 1345 | return templ_7745c5c3_Err | ||
| 1314 | } | 1346 | } | ||
| 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=\"") | 1347 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "</h1><form class=\"state-act\" data-js data-passkey-form><button type=\"submit\" class=\"btn primary\" data-passkey=\"login\" data-begin=\"") |
| 1316 | if templ_7745c5c3_Err != nil { | 1348 | if templ_7745c5c3_Err != nil { | ||
| 1317 | return templ_7745c5c3_Err | 1349 | return templ_7745c5c3_Err | ||
| 1318 | } | 1350 | } | ||
| 1319 | - | var templ_7745c5c3_Var67 string | 1351 | + | var templ_7745c5c3_Var69 string |
| 1320 | - | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin) | 1352 | + | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin) |
| 1321 | if templ_7745c5c3_Err != nil { | 1353 | if templ_7745c5c3_Err != nil { | ||
| 1322 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 87} | 1354 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 252, Col: 87} |
| 1323 | } | 1355 | } | ||
| 1324 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67) | 1356 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69) |
| 1325 | if templ_7745c5c3_Err != nil { | 1357 | if templ_7745c5c3_Err != nil { | ||
| 1326 | return templ_7745c5c3_Err | 1358 | return templ_7745c5c3_Err | ||
| 1327 | } | 1359 | } | ||
| 1328 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "\" data-finish=\"") | 1360 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "\" data-finish=\"") |
| 1329 | if templ_7745c5c3_Err != nil { | 1361 | if templ_7745c5c3_Err != nil { | ||
| 1330 | return templ_7745c5c3_Err | 1362 | return templ_7745c5c3_Err | ||
| 1331 | } | 1363 | } | ||
| 1332 | - | var templ_7745c5c3_Var68 string | 1364 | + | var templ_7745c5c3_Var70 string |
| 1333 | - | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish) | 1365 | + | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish) |
| 1334 | if templ_7745c5c3_Err != nil { | 1366 | if templ_7745c5c3_Err != nil { | ||
| 1335 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 112} | 1367 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 252, Col: 112} |
| 1336 | } | 1368 | } | ||
| 1337 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var68) | 1369 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var70) |
| 1338 | if templ_7745c5c3_Err != nil { | 1370 | if templ_7745c5c3_Err != nil { | ||
| 1339 | return templ_7745c5c3_Err | 1371 | return templ_7745c5c3_Err | ||
| 1340 | } | 1372 | } | ||
| 1341 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "\">") | 1373 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "\">") |
| 1342 | if templ_7745c5c3_Err != nil { | 1374 | if templ_7745c5c3_Err != nil { | ||
| 1343 | return templ_7745c5c3_Err | 1375 | return templ_7745c5c3_Err | ||
| 1344 | } | 1376 | } | ||
| 1345 | - | var templ_7745c5c3_Var69 string | 1377 | + | var templ_7745c5c3_Var71 string |
| 1346 | - | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button) | 1378 | + | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button) |
| 1347 | if templ_7745c5c3_Err != nil { | 1379 | if templ_7745c5c3_Err != nil { | ||
| 1348 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 248, Col: 125} | 1380 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 252, Col: 125} |
| 1349 | } | 1381 | } | ||
| 1350 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | 1382 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) |
| 1351 | if templ_7745c5c3_Err != nil { | 1383 | if templ_7745c5c3_Err != nil { | ||
| 1352 | return templ_7745c5c3_Err | 1384 | return templ_7745c5c3_Err | ||
| 1353 | } | 1385 | } | ||
| 1354 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>") | 1386 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>") |
| 1355 | if templ_7745c5c3_Err != nil { | 1387 | if templ_7745c5c3_Err != nil { | ||
| 1356 | return templ_7745c5c3_Err | 1388 | return templ_7745c5c3_Err | ||
| 1357 | } | 1389 | } | ||
| 1358 | for _, line := range p.Lines { | 1390 | for _, line := range p.Lines { | ||
| 1359 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<p class=\"dim\">") | 1391 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<p class=\"dim\">") |
| 1360 | if templ_7745c5c3_Err != nil { | 1392 | if templ_7745c5c3_Err != nil { | ||
| 1361 | return templ_7745c5c3_Err | 1393 | return templ_7745c5c3_Err | ||
| 1362 | } | 1394 | } | ||
| 1363 | - | var templ_7745c5c3_Var70 string | 1395 | + | var templ_7745c5c3_Var72 string |
| 1364 | - | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(line) | 1396 | + | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(line) |
| 1365 | if templ_7745c5c3_Err != nil { | 1397 | if templ_7745c5c3_Err != nil { | ||
| 1366 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 252, Col: 25} | 1398 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 256, Col: 25} |
| 1367 | } | 1399 | } | ||
| 1368 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | 1400 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) |
| 1369 | if templ_7745c5c3_Err != nil { | 1401 | if templ_7745c5c3_Err != nil { | ||
| 1370 | return templ_7745c5c3_Err | 1402 | return templ_7745c5c3_Err | ||
| 1371 | } | 1403 | } | ||
| 1372 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "</p>") | 1404 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</p>") |
| 1373 | if templ_7745c5c3_Err != nil { | 1405 | if templ_7745c5c3_Err != nil { | ||
| 1374 | return templ_7745c5c3_Err | 1406 | return templ_7745c5c3_Err | ||
| 1375 | } | 1407 | } | ||
| 1376 | } | 1408 | } | ||
| 1377 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>") | 1409 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>") |
| 1378 | if templ_7745c5c3_Err != nil { | 1410 | if templ_7745c5c3_Err != nil { | ||
| 1379 | return templ_7745c5c3_Err | 1411 | return templ_7745c5c3_Err | ||
| 1380 | } | 1412 | } | ||
| 1381 | return nil | 1413 | return nil | ||
| 1382 | }) | 1414 | }) | ||
| 1383 | - | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var65), templ_7745c5c3_Buffer) | 1415 | + | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var67), templ_7745c5c3_Buffer) |
| 1384 | if templ_7745c5c3_Err != nil { | 1416 | if templ_7745c5c3_Err != nil { | ||
| 1385 | return templ_7745c5c3_Err | 1417 | return templ_7745c5c3_Err | ||
| 1386 | } | 1418 | } | ||
| @@ -1405,12 +1437,12 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component { | |||||
| 1405 | }() | 1437 | }() | ||
| 1406 | } | 1438 | } | ||
| 1407 | ctx = templ.InitializeContext(ctx) | 1439 | ctx = templ.InitializeContext(ctx) | ||
| 1408 | - | templ_7745c5c3_Var71 := templ.GetChildren(ctx) | 1440 | + | templ_7745c5c3_Var73 := templ.GetChildren(ctx) |
| 1409 | - | if templ_7745c5c3_Var71 == nil { | 1441 | + | if templ_7745c5c3_Var73 == nil { |
| 1410 | - | templ_7745c5c3_Var71 = templ.NopComponent | 1442 | + | templ_7745c5c3_Var73 = templ.NopComponent |
| 1411 | } | 1443 | } | ||
| 1412 | ctx = templ.ClearChildren(ctx) | 1444 | ctx = templ.ClearChildren(ctx) | ||
| 1413 | - | templ_7745c5c3_Var72 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | 1445 | + | templ_7745c5c3_Var74 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { |
| 1414 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | 1446 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | ||
| 1415 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | 1447 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | ||
| 1416 | if !templ_7745c5c3_IsBuffer { | 1448 | if !templ_7745c5c3_IsBuffer { | ||
| @@ -1422,231 +1454,231 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component { | |||||
| 1422 | }() | 1454 | }() | ||
| 1423 | } | 1455 | } | ||
| 1424 | ctx = templ.InitializeContext(ctx) | 1456 | ctx = templ.InitializeContext(ctx) | ||
| 1425 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">") | 1457 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">") |
| 1426 | if templ_7745c5c3_Err != nil { | 1458 | if templ_7745c5c3_Err != nil { | ||
| 1427 | return templ_7745c5c3_Err | 1459 | return templ_7745c5c3_Err | ||
| 1428 | } | 1460 | } | ||
| 1429 | - | var templ_7745c5c3_Var73 string | 1461 | + | var templ_7745c5c3_Var75 string |
| 1430 | - | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows))) | 1462 | + | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows))) |
| 1431 | if templ_7745c5c3_Err != nil { | 1463 | if templ_7745c5c3_Err != nil { | ||
| 1432 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 262, Col: 81} | 1464 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 266, Col: 81} |
| 1433 | } | 1465 | } | ||
| 1434 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) | 1466 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) |
| 1435 | if templ_7745c5c3_Err != nil { | 1467 | if templ_7745c5c3_Err != nil { | ||
| 1436 | return templ_7745c5c3_Err | 1468 | return templ_7745c5c3_Err | ||
| 1437 | } | 1469 | } | ||
| 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>") | 1470 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</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>") |
| 1439 | if templ_7745c5c3_Err != nil { | 1471 | if templ_7745c5c3_Err != nil { | ||
| 1440 | return templ_7745c5c3_Err | 1472 | return templ_7745c5c3_Err | ||
| 1441 | } | 1473 | } | ||
| 1442 | if len(p.Rows) > 0 { | 1474 | if len(p.Rows) > 0 { | ||
| 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>") | 1475 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<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>") |
| 1444 | if templ_7745c5c3_Err != nil { | 1476 | if templ_7745c5c3_Err != nil { | ||
| 1445 | return templ_7745c5c3_Err | 1477 | return templ_7745c5c3_Err | ||
| 1446 | } | 1478 | } | ||
| 1447 | for _, r := range p.Rows { | 1479 | for _, r := range p.Rows { | ||
| 1448 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "<tr><td class=\"c-name\">") | 1480 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "<tr><td class=\"c-name\">") |
| 1449 | if templ_7745c5c3_Err != nil { | 1481 | if templ_7745c5c3_Err != nil { | ||
| 1450 | return templ_7745c5c3_Err | 1482 | return templ_7745c5c3_Err | ||
| 1451 | } | 1483 | } | ||
| 1452 | - | var templ_7745c5c3_Var74 string | 1484 | + | var templ_7745c5c3_Var76 string |
| 1453 | - | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | 1485 | + | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) |
| 1454 | if templ_7745c5c3_Err != nil { | 1486 | if templ_7745c5c3_Err != nil { | ||
| 1455 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 278, Col: 34} | 1487 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 282, Col: 34} |
| 1456 | } | 1488 | } | ||
| 1457 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74)) | 1489 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) |
| 1458 | if templ_7745c5c3_Err != nil { | 1490 | if templ_7745c5c3_Err != nil { | ||
| 1459 | return templ_7745c5c3_Err | 1491 | return templ_7745c5c3_Err | ||
| 1460 | } | 1492 | } | ||
| 1461 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</td><td class=\"c-when nowrap\"><time") | 1493 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</td><td class=\"c-when nowrap\"><time") |
| 1462 | if templ_7745c5c3_Err != nil { | 1494 | if templ_7745c5c3_Err != nil { | ||
| 1463 | return templ_7745c5c3_Err | 1495 | return templ_7745c5c3_Err | ||
| 1464 | } | 1496 | } | ||
| 1465 | if r.CreatedAt != "" { | 1497 | if r.CreatedAt != "" { | ||
| 1466 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, " datetime=\"") | 1498 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, " datetime=\"") |
| 1467 | if templ_7745c5c3_Err != nil { | 1499 | if templ_7745c5c3_Err != nil { | ||
| 1468 | return templ_7745c5c3_Err | 1500 | return templ_7745c5c3_Err | ||
| 1469 | } | 1501 | } | ||
| 1470 | - | var templ_7745c5c3_Var75 string | 1502 | + | var templ_7745c5c3_Var77 string |
| 1471 | - | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt) | 1503 | + | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt) |
| 1472 | if templ_7745c5c3_Err != nil { | 1504 | if templ_7745c5c3_Err != nil { | ||
| 1473 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 282, Col: 32} | 1505 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 286, Col: 32} |
| 1474 | } | 1506 | } | ||
| 1475 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75) | 1507 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77) |
| 1476 | if templ_7745c5c3_Err != nil { | 1508 | if templ_7745c5c3_Err != nil { | ||
| 1477 | return templ_7745c5c3_Err | 1509 | return templ_7745c5c3_Err | ||
| 1478 | } | 1510 | } | ||
| 1479 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"") | 1511 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\"") |
| 1480 | if templ_7745c5c3_Err != nil { | 1512 | if templ_7745c5c3_Err != nil { | ||
| 1481 | return templ_7745c5c3_Err | 1513 | return templ_7745c5c3_Err | ||
| 1482 | } | 1514 | } | ||
| 1483 | } | 1515 | } | ||
| 1484 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, ">") | 1516 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, ">") |
| 1485 | if templ_7745c5c3_Err != nil { | 1517 | if templ_7745c5c3_Err != nil { | ||
| 1486 | return templ_7745c5c3_Err | 1518 | return templ_7745c5c3_Err | ||
| 1487 | } | 1519 | } | ||
| 1488 | - | var templ_7745c5c3_Var76 string | 1520 | + | var templ_7745c5c3_Var78 string |
| 1489 | - | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created) | 1521 | + | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created) |
| 1490 | if templ_7745c5c3_Err != nil { | 1522 | if templ_7745c5c3_Err != nil { | ||
| 1491 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 284, Col: 20} | 1523 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 288, Col: 20} |
| 1492 | } | 1524 | } | ||
| 1493 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) | 1525 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) |
| 1494 | if templ_7745c5c3_Err != nil { | 1526 | if templ_7745c5c3_Err != nil { | ||
| 1495 | return templ_7745c5c3_Err | 1527 | return templ_7745c5c3_Err | ||
| 1496 | } | 1528 | } | ||
| 1497 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "</time></td><td class=\"c-when nowrap\"><time") | 1529 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</time></td><td class=\"c-when nowrap\"><time") |
| 1498 | if templ_7745c5c3_Err != nil { | 1530 | if templ_7745c5c3_Err != nil { | ||
| 1499 | return templ_7745c5c3_Err | 1531 | return templ_7745c5c3_Err | ||
| 1500 | } | 1532 | } | ||
| 1501 | if r.LastUsedAt != "" { | 1533 | if r.LastUsedAt != "" { | ||
| 1502 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, " datetime=\"") | 1534 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, " datetime=\"") |
| 1503 | if templ_7745c5c3_Err != nil { | 1535 | if templ_7745c5c3_Err != nil { | ||
| 1504 | return templ_7745c5c3_Err | 1536 | return templ_7745c5c3_Err | ||
| 1505 | } | 1537 | } | ||
| 1506 | - | var templ_7745c5c3_Var77 string | 1538 | + | var templ_7745c5c3_Var79 string |
| 1507 | - | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt) | 1539 | + | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt) |
| 1508 | if templ_7745c5c3_Err != nil { | 1540 | if templ_7745c5c3_Err != nil { | ||
| 1509 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 289, Col: 33} | 1541 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 293, Col: 33} |
| 1510 | } | 1542 | } | ||
| 1511 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77) | 1543 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79) |
| 1512 | if templ_7745c5c3_Err != nil { | 1544 | if templ_7745c5c3_Err != nil { | ||
| 1513 | return templ_7745c5c3_Err | 1545 | return templ_7745c5c3_Err | ||
| 1514 | } | 1546 | } | ||
| 1515 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\"") | 1547 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\"") |
| 1516 | if templ_7745c5c3_Err != nil { | 1548 | if templ_7745c5c3_Err != nil { | ||
| 1517 | return templ_7745c5c3_Err | 1549 | return templ_7745c5c3_Err | ||
| 1518 | } | 1550 | } | ||
| 1519 | } | 1551 | } | ||
| 1520 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, ">") | 1552 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, ">") |
| 1521 | if templ_7745c5c3_Err != nil { | 1553 | if templ_7745c5c3_Err != nil { | ||
| 1522 | return templ_7745c5c3_Err | 1554 | return templ_7745c5c3_Err | ||
| 1523 | } | 1555 | } | ||
| 1524 | - | var templ_7745c5c3_Var78 string | 1556 | + | var templ_7745c5c3_Var80 string |
| 1525 | - | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed) | 1557 | + | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed) |
| 1526 | if templ_7745c5c3_Err != nil { | 1558 | if templ_7745c5c3_Err != nil { | ||
| 1527 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 291, Col: 21} | 1559 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 295, Col: 21} |
| 1528 | } | 1560 | } | ||
| 1529 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) | 1561 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var80)) |
| 1530 | if templ_7745c5c3_Err != nil { | 1562 | if templ_7745c5c3_Err != nil { | ||
| 1531 | return templ_7745c5c3_Err | 1563 | return templ_7745c5c3_Err | ||
| 1532 | } | 1564 | } | ||
| 1533 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</time></td><td class=\"c-sync\">") | 1565 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</time></td><td class=\"c-sync\">") |
| 1534 | if templ_7745c5c3_Err != nil { | 1566 | if templ_7745c5c3_Err != nil { | ||
| 1535 | return templ_7745c5c3_Err | 1567 | return templ_7745c5c3_Err | ||
| 1536 | } | 1568 | } | ||
| 1537 | if r.Synced { | 1569 | if r.Synced { | ||
| 1538 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "yes") | 1570 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "yes") |
| 1539 | if templ_7745c5c3_Err != nil { | 1571 | if templ_7745c5c3_Err != nil { | ||
| 1540 | return templ_7745c5c3_Err | 1572 | return templ_7745c5c3_Err | ||
| 1541 | } | 1573 | } | ||
| 1542 | } else { | 1574 | } else { | ||
| 1543 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "no") | 1575 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "no") |
| 1544 | if templ_7745c5c3_Err != nil { | 1576 | if templ_7745c5c3_Err != nil { | ||
| 1545 | return templ_7745c5c3_Err | 1577 | return templ_7745c5c3_Err | ||
| 1546 | } | 1578 | } | ||
| 1547 | } | 1579 | } | ||
| 1548 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</td><td class=\"c-act\"><form method=\"post\" action=\"") | 1580 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</td><td class=\"c-act\"><form method=\"post\" action=\"") |
| 1549 | if templ_7745c5c3_Err != nil { | 1581 | if templ_7745c5c3_Err != nil { | ||
| 1550 | return templ_7745c5c3_Err | 1582 | return templ_7745c5c3_Err | ||
| 1551 | } | 1583 | } | ||
| 1552 | - | var templ_7745c5c3_Var79 templ.SafeURL | 1584 | + | var templ_7745c5c3_Var81 templ.SafeURL |
| 1553 | - | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove)) | 1585 | + | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove)) |
| 1554 | if templ_7745c5c3_Err != nil { | 1586 | if templ_7745c5c3_Err != nil { | ||
| 1555 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 301, Col: 60} | 1587 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 305, Col: 60} |
| 1556 | } | 1588 | } | ||
| 1557 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79)) | 1589 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) |
| 1558 | if templ_7745c5c3_Err != nil { | 1590 | if templ_7745c5c3_Err != nil { | ||
| 1559 | return templ_7745c5c3_Err | 1591 | return templ_7745c5c3_Err | ||
| 1560 | } | 1592 | } | ||
| 1561 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"") | 1593 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"") |
| 1562 | if templ_7745c5c3_Err != nil { | 1594 | if templ_7745c5c3_Err != nil { | ||
| 1563 | return templ_7745c5c3_Err | 1595 | return templ_7745c5c3_Err | ||
| 1564 | } | 1596 | } | ||
| 1565 | - | var templ_7745c5c3_Var80 string | 1597 | + | var templ_7745c5c3_Var82 string |
| 1566 | - | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name) | 1598 | + | templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name) |
| 1567 | if templ_7745c5c3_Err != nil { | 1599 | if templ_7745c5c3_Err != nil { | ||
| 1568 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 302, Col: 89} | 1600 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 306, Col: 89} |
| 1569 | } | 1601 | } | ||
| 1570 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80) | 1602 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82) |
| 1571 | if templ_7745c5c3_Err != nil { | 1603 | if templ_7745c5c3_Err != nil { | ||
| 1572 | return templ_7745c5c3_Err | 1604 | return templ_7745c5c3_Err | ||
| 1573 | } | 1605 | } | ||
| 1574 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "\">Remove</button></form></td></tr>") | 1606 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\">Remove</button></form></td></tr>") |
| 1575 | if templ_7745c5c3_Err != nil { | 1607 | if templ_7745c5c3_Err != nil { | ||
| 1576 | return templ_7745c5c3_Err | 1608 | return templ_7745c5c3_Err | ||
| 1577 | } | 1609 | } | ||
| 1578 | } | 1610 | } | ||
| 1579 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</tbody></table>") | 1611 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</tbody></table>") |
| 1580 | if templ_7745c5c3_Err != nil { | 1612 | if templ_7745c5c3_Err != nil { | ||
| 1581 | return templ_7745c5c3_Err | 1613 | return templ_7745c5c3_Err | ||
| 1582 | } | 1614 | } | ||
| 1583 | } else { | 1615 | } else { | ||
| 1584 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<p class=\"lede dim\">no passkeys yet.</p>") | 1616 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<p class=\"lede dim\">no passkeys yet.</p>") |
| 1585 | if templ_7745c5c3_Err != nil { | 1617 | if templ_7745c5c3_Err != nil { | ||
| 1586 | return templ_7745c5c3_Err | 1618 | return templ_7745c5c3_Err | ||
| 1587 | } | 1619 | } | ||
| 1588 | } | 1620 | } | ||
| 1589 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, " ") | 1621 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, " ") |
| 1590 | if templ_7745c5c3_Err != nil { | 1622 | if templ_7745c5c3_Err != nil { | ||
| 1591 | return templ_7745c5c3_Err | 1623 | return templ_7745c5c3_Err | ||
| 1592 | } | 1624 | } | ||
| 1593 | if p.Full { | 1625 | if p.Full { | ||
| 1594 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<p class=\"dim\">") | 1626 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "<p class=\"dim\">") |
| 1595 | if templ_7745c5c3_Err != nil { | 1627 | if templ_7745c5c3_Err != nil { | ||
| 1596 | return templ_7745c5c3_Err | 1628 | return templ_7745c5c3_Err | ||
| 1597 | } | 1629 | } | ||
| 1598 | - | var templ_7745c5c3_Var81 string | 1630 | + | var templ_7745c5c3_Var83 string |
| 1599 | - | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max)) | 1631 | + | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max)) |
| 1600 | if templ_7745c5c3_Err != nil { | 1632 | if templ_7745c5c3_Err != nil { | ||
| 1601 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 313, Col: 31} | 1633 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 317, Col: 31} |
| 1602 | } | 1634 | } | ||
| 1603 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) | 1635 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83)) |
| 1604 | if templ_7745c5c3_Err != nil { | 1636 | if templ_7745c5c3_Err != nil { | ||
| 1605 | return templ_7745c5c3_Err | 1637 | return templ_7745c5c3_Err | ||
| 1606 | } | 1638 | } | ||
| 1607 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, " passkeys are registered, the most there can be. Remove one to add another.</p>") | 1639 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, " passkeys are registered, the most there can be. Remove one to add another.</p>") |
| 1608 | if templ_7745c5c3_Err != nil { | 1640 | if templ_7745c5c3_Err != nil { | ||
| 1609 | return templ_7745c5c3_Err | 1641 | return templ_7745c5c3_Err | ||
| 1610 | } | 1642 | } | ||
| 1611 | } else { | 1643 | } else { | ||
| 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=\"") | 1644 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<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=\"") |
| 1613 | if templ_7745c5c3_Err != nil { | 1645 | if templ_7745c5c3_Err != nil { | ||
| 1614 | return templ_7745c5c3_Err | 1646 | return templ_7745c5c3_Err | ||
| 1615 | } | 1647 | } | ||
| 1616 | - | var templ_7745c5c3_Var82 string | 1648 | + | var templ_7745c5c3_Var84 string |
| 1617 | - | templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin) | 1649 | + | templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin) |
| 1618 | if templ_7745c5c3_Err != nil { | 1650 | if templ_7745c5c3_Err != nil { | ||
| 1619 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 318, Col: 90} | 1651 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 322, Col: 90} |
| 1620 | } | 1652 | } | ||
| 1621 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82) | 1653 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84) |
| 1622 | if templ_7745c5c3_Err != nil { | 1654 | if templ_7745c5c3_Err != nil { | ||
| 1623 | return templ_7745c5c3_Err | 1655 | return templ_7745c5c3_Err | ||
| 1624 | } | 1656 | } | ||
| 1625 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "\" data-finish=\"") | 1657 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "\" data-finish=\"") |
| 1626 | if templ_7745c5c3_Err != nil { | 1658 | if templ_7745c5c3_Err != nil { | ||
| 1627 | return templ_7745c5c3_Err | 1659 | return templ_7745c5c3_Err | ||
| 1628 | } | 1660 | } | ||
| 1629 | - | var templ_7745c5c3_Var83 string | 1661 | + | var templ_7745c5c3_Var85 string |
| 1630 | - | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish) | 1662 | + | templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish) |
| 1631 | if templ_7745c5c3_Err != nil { | 1663 | if templ_7745c5c3_Err != nil { | ||
| 1632 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 318, Col: 115} | 1664 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 322, Col: 115} |
| 1633 | } | 1665 | } | ||
| 1634 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83) | 1666 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var85) |
| 1635 | if templ_7745c5c3_Err != nil { | 1667 | if templ_7745c5c3_Err != nil { | ||
| 1636 | return templ_7745c5c3_Err | 1668 | return templ_7745c5c3_Err | ||
| 1637 | } | 1669 | } | ||
| 1638 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "\">Add a passkey</button></form>") | 1670 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "\">Add a passkey</button></form>") |
| 1639 | if templ_7745c5c3_Err != nil { | 1671 | if templ_7745c5c3_Err != nil { | ||
| 1640 | return templ_7745c5c3_Err | 1672 | return templ_7745c5c3_Err | ||
| 1641 | } | 1673 | } | ||
| 1642 | } | 1674 | } | ||
| 1643 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>") | 1675 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>") |
| 1644 | if templ_7745c5c3_Err != nil { | 1676 | if templ_7745c5c3_Err != nil { | ||
| 1645 | return templ_7745c5c3_Err | 1677 | return templ_7745c5c3_Err | ||
| 1646 | } | 1678 | } | ||
| 1647 | return nil | 1679 | return nil | ||
| 1648 | }) | 1680 | }) | ||
| 1649 | - | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var72), templ_7745c5c3_Buffer) | 1681 | + | templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var74), templ_7745c5c3_Buffer) |
| 1650 | if templ_7745c5c3_Err != nil { | 1682 | if templ_7745c5c3_Err != nil { | ||
| 1651 | return templ_7745c5c3_Err | 1683 | return templ_7745c5c3_Err | ||
| 1652 | } | 1684 | } | ||
M internal/adapters/web/pathcache.go +16 -6
| @@ -70,6 +70,14 @@ func (c *pathCache) getLocked(id string) ([]byte, bool) { | |||
| 70 | 70 | return el.Value.(*pathEntry).body, true | |
| 71 | 71 | } | |
| 72 | 72 | ||
| 73 | + | // entryOverhead is what an entry costs besides its id and body: the list | |
| 74 | + | // element, the entry and the map slot. Counted so that tiny bodies (a cached | |
| 75 | + | // "no license") cannot pile up far past the budget. | |
| 76 | + | const entryOverhead = 128 | |
| 77 | + | ||
| 78 | + | // cost is what an entry counts against the budget. | |
| 79 | + | func (e *pathEntry) cost() int { return len(e.id) + len(e.body) + entryOverhead } | |
| 80 | + | ||
| 73 | 81 | // putLocked stores a list, evicting the least recently used ones to stay in | |
| 74 | 82 | // budget. A list over a quarter of the budget is not stored. | |
| 75 | 83 | func (c *pathCache) putLocked(id string, body []byte) { | |
| @@ -77,20 +85,22 @@ func (c *pathCache) putLocked(id string, body []byte) { | |||
| 77 | 85 | return | |
| 78 | 86 | } | |
| 79 | 87 | if el, ok := c.byID[id]; ok { | |
| 80 | - | c.size -= len(el.Value.(*pathEntry).body) | |
| 81 | - | el.Value.(*pathEntry).body = body | |
| 82 | - | c.size += len(body) | |
| 88 | + | e := el.Value.(*pathEntry) | |
| 89 | + | c.size -= e.cost() | |
| 90 | + | e.body = body | |
| 91 | + | c.size += e.cost() | |
| 83 | 92 | c.lru.MoveToFront(el) | |
| 84 | 93 | } else { | |
| 85 | - | c.byID[id] = c.lru.PushFront(&pathEntry{id: id, body: body}) | |
| 86 | - | c.size += len(body) | |
| 94 | + | e := &pathEntry{id: id, body: body} | |
| 95 | + | c.byID[id] = c.lru.PushFront(e) | |
| 96 | + | c.size += e.cost() | |
| 87 | 97 | } | |
| 88 | 98 | for c.size > c.budget { | |
| 89 | 99 | el := c.lru.Back() | |
| 90 | 100 | e := el.Value.(*pathEntry) | |
| 91 | 101 | c.lru.Remove(el) | |
| 92 | 102 | delete(c.byID, e.id) | |
| 93 | - | c.size -= len(e.body) | |
| 103 | + | c.size -= e.cost() | |
| 94 | 104 | } | |
| 95 | 105 | } | |
| 96 | 106 | ||
| @@ -70,6 +70,14 @@ func (c *pathCache) getLocked(id string) ([]byte, bool) { | |||||
| 70 | return el.Value.(*pathEntry).body, true | 70 | return el.Value.(*pathEntry).body, true | ||
| 71 | } | 71 | } | ||
| 72 | 72 | ||||
| 73 | + | // entryOverhead is what an entry costs besides its id and body: the list | |||
| 74 | + | // element, the entry and the map slot. Counted so that tiny bodies (a cached | |||
| 75 | + | // "no license") cannot pile up far past the budget. | |||
| 76 | + | const entryOverhead = 128 | |||
| 77 | + | ||||
| 78 | + | // cost is what an entry counts against the budget. | |||
| 79 | + | func (e *pathEntry) cost() int { return len(e.id) + len(e.body) + entryOverhead } | |||
| 80 | + | ||||
| 73 | // putLocked stores a list, evicting the least recently used ones to stay in | 81 | // putLocked stores a list, evicting the least recently used ones to stay in | ||
| 74 | // budget. A list over a quarter of the budget is not stored. | 82 | // budget. A list over a quarter of the budget is not stored. | ||
| 75 | func (c *pathCache) putLocked(id string, body []byte) { | 83 | func (c *pathCache) putLocked(id string, body []byte) { | ||
| @@ -77,20 +85,22 @@ func (c *pathCache) putLocked(id string, body []byte) { | |||||
| 77 | return | 85 | return | ||
| 78 | } | 86 | } | ||
| 79 | if el, ok := c.byID[id]; ok { | 87 | if el, ok := c.byID[id]; ok { | ||
| 80 | - | c.size -= len(el.Value.(*pathEntry).body) | 88 | + | e := el.Value.(*pathEntry) |
| 81 | - | el.Value.(*pathEntry).body = body | 89 | + | c.size -= e.cost() |
| 82 | - | c.size += len(body) | 90 | + | e.body = body |
| 91 | + | c.size += e.cost() | |||
| 83 | c.lru.MoveToFront(el) | 92 | c.lru.MoveToFront(el) | ||
| 84 | } else { | 93 | } else { | ||
| 85 | - | c.byID[id] = c.lru.PushFront(&pathEntry{id: id, body: body}) | 94 | + | e := &pathEntry{id: id, body: body} |
| 86 | - | c.size += len(body) | 95 | + | c.byID[id] = c.lru.PushFront(e) |
| 96 | + | c.size += e.cost() | |||
| 87 | } | 97 | } | ||
| 88 | for c.size > c.budget { | 98 | for c.size > c.budget { | ||
| 89 | el := c.lru.Back() | 99 | el := c.lru.Back() | ||
| 90 | e := el.Value.(*pathEntry) | 100 | e := el.Value.(*pathEntry) | ||
| 91 | c.lru.Remove(el) | 101 | c.lru.Remove(el) | ||
| 92 | delete(c.byID, e.id) | 102 | delete(c.byID, e.id) | ||
| 93 | - | c.size -= len(e.body) | 103 | + | c.size -= e.cost() |
| 94 | } | 104 | } | ||
| 95 | } | 105 | } | ||
| 96 | 106 | ||||
M internal/adapters/web/pathcache_test.go +34 -17
| @@ -30,55 +30,72 @@ func (c *pathCache) has(id string) bool { | |||
| 30 | 30 | return ok | |
| 31 | 31 | } | |
| 32 | 32 | ||
| 33 | + | // entry is the budget an entry of a one-letter id and an n-byte body takes. | |
| 34 | + | func entry(n int) int { return 1 + n + entryOverhead } | |
| 35 | + | ||
| 33 | 36 | func TestPathCacheEvictsLeastRecentlyUsed(t *testing.T) { | |
| 34 | 37 | t.Parallel() | |
| 35 | - | c := newPathCache(100, 1) | |
| 38 | + | budget := 4 * entry(100) | |
| 39 | + | c := newPathCache(budget, 1) | |
| 36 | 40 | blob := func(n int) []byte { return make([]byte, n) } | |
| 37 | - | c.store("a", blob(25)) | |
| 38 | - | c.store("b", blob(25)) | |
| 39 | - | c.store("e", blob(25)) | |
| 41 | + | c.store("a", blob(100)) | |
| 42 | + | c.store("b", blob(100)) | |
| 43 | + | c.store("e", blob(100)) | |
| 40 | 44 | require.True(t, c.has("a")) // a is now the most recent | |
| 41 | - | c.store("c", blob(25)) // 100 fit | |
| 42 | - | c.store("d", blob(10)) // 110 > 100: b, the least recent, goes | |
| 45 | + | c.store("c", blob(100)) // four fit | |
| 46 | + | c.store("d", blob(10)) // one more: b, the least recent, goes | |
| 43 | 47 | assert.False(t, c.has("b"), "b was evicted") | |
| 44 | 48 | assert.True(t, c.has("a") && c.has("c") && c.has("d") && c.has("e")) | |
| 45 | - | assert.Equal(t, 85, c.size) | |
| 49 | + | assert.Equal(t, 3*entry(100)+entry(10), c.size) | |
| 46 | 50 | ||
| 47 | 51 | // Replacing an entry accounts for the old size. | |
| 48 | 52 | c.store("a", blob(10)) | |
| 49 | - | assert.Equal(t, 70, c.size) | |
| 53 | + | assert.Equal(t, 2*entry(100)+2*entry(10), c.size) | |
| 50 | 54 | ||
| 51 | 55 | // A big one pushes out as many as needed. | |
| 52 | - | c.store("f", blob(25)) | |
| 53 | - | c.store("g", blob(25)) | |
| 54 | - | assert.LessOrEqual(t, c.size, 100) | |
| 56 | + | c.store("f", blob(100)) | |
| 57 | + | c.store("g", blob(100)) | |
| 58 | + | assert.LessOrEqual(t, c.size, budget) | |
| 55 | 59 | assert.True(t, c.has("g")) | |
| 56 | 60 | } | |
| 57 | 61 | ||
| 62 | + | // Tiny bodies (a cached "no license") count their id and overhead too, so | |
| 63 | + | // their number stays bounded by the budget. | |
| 64 | + | func TestPathCacheBoundsTinyEntries(t *testing.T) { | |
| 65 | + | t.Parallel() | |
| 66 | + | c := newPathCache(1<<12, 1) | |
| 67 | + | for i := range 10_000 { | |
| 68 | + | c.store(fmt.Sprintf("repo\x00%040d", i), []byte("null")) | |
| 69 | + | } | |
| 70 | + | assert.LessOrEqual(t, c.size, 1<<12) | |
| 71 | + | assert.Less(t, len(c.byID), (1<<12)/entryOverhead+1) | |
| 72 | + | assert.Equal(t, len(c.byID), c.lru.Len()) | |
| 73 | + | } | |
| 74 | + | ||
| 58 | 75 | // A list over a quarter of the budget is not kept, since it would push out the | |
| 59 | 76 | // others; one at the limit is. | |
| 60 | 77 | func TestPathCacheSkipsOversizedEntries(t *testing.T) { | |
| 61 | 78 | t.Parallel() | |
| 62 | - | c := newPathCache(100, 1) | |
| 79 | + | c := newPathCache(1000, 1) | |
| 63 | 80 | var builds atomic.Int32 | |
| 64 | 81 | big := func(n int) func() ([]byte, error) { | |
| 65 | 82 | return func() ([]byte, error) { builds.Add(1); return make([]byte, n), nil } | |
| 66 | 83 | } | |
| 67 | 84 | for range 2 { | |
| 68 | - | b, err := c.load(context.Background(), "r", "big", big(26)) | |
| 85 | + | b, err := c.load(context.Background(), "r", "big", big(251)) | |
| 69 | 86 | require.NoError(t, err) | |
| 70 | - | assert.Len(t, b, 26, "the caller still gets the list") | |
| 87 | + | assert.Len(t, b, 251, "the caller still gets the list") | |
| 71 | 88 | } | |
| 72 | 89 | assert.EqualValues(t, 2, builds.Load(), "not cached, so built again") | |
| 73 | 90 | assert.False(t, c.has("big")) | |
| 74 | 91 | assert.Zero(t, c.size) | |
| 75 | 92 | ||
| 76 | 93 | for range 2 { | |
| 77 | - | _, err := c.load(context.Background(), "r", "fits", big(25)) | |
| 94 | + | _, err := c.load(context.Background(), "r", "fits", big(250)) | |
| 78 | 95 | require.NoError(t, err) | |
| 79 | 96 | } | |
| 80 | 97 | assert.EqualValues(t, 3, builds.Load(), "at a quarter: kept") | |
| 81 | - | assert.Equal(t, 25, c.size) | |
| 98 | + | assert.Equal(t, len("fits")+250+entryOverhead, c.size) | |
| 82 | 99 | } | |
| 83 | 100 | ||
| 84 | 101 | func TestPathCacheConcurrent(t *testing.T) { | |
| @@ -99,7 +116,7 @@ func TestPathCacheConcurrent(t *testing.T) { | |||
| 99 | 116 | assert.LessOrEqual(t, c.size, 1000) | |
| 100 | 117 | sum := 0 | |
| 101 | 118 | for _, el := range c.byID { | |
| 102 | - | sum += len(el.Value.(*pathEntry).body) | |
| 119 | + | sum += el.Value.(*pathEntry).cost() | |
| 103 | 120 | } | |
| 104 | 121 | assert.Equal(t, sum, c.size, "the size is the sum of the entries") | |
| 105 | 122 | assert.Equal(t, len(c.byID), c.lru.Len()) | |
| @@ -30,55 +30,72 @@ func (c *pathCache) has(id string) bool { | |||||
| 30 | return ok | 30 | return ok | ||
| 31 | } | 31 | } | ||
| 32 | 32 | ||||
| 33 | + | // entry is the budget an entry of a one-letter id and an n-byte body takes. | |||
| 34 | + | func entry(n int) int { return 1 + n + entryOverhead } | |||
| 35 | + | ||||
| 33 | func TestPathCacheEvictsLeastRecentlyUsed(t *testing.T) { | 36 | func TestPathCacheEvictsLeastRecentlyUsed(t *testing.T) { | ||
| 34 | t.Parallel() | 37 | t.Parallel() | ||
| 35 | - | c := newPathCache(100, 1) | 38 | + | budget := 4 * entry(100) |
| 39 | + | c := newPathCache(budget, 1) | |||
| 36 | blob := func(n int) []byte { return make([]byte, n) } | 40 | blob := func(n int) []byte { return make([]byte, n) } | ||
| 37 | - | c.store("a", blob(25)) | 41 | + | c.store("a", blob(100)) |
| 38 | - | c.store("b", blob(25)) | 42 | + | c.store("b", blob(100)) |
| 39 | - | c.store("e", blob(25)) | 43 | + | c.store("e", blob(100)) |
| 40 | require.True(t, c.has("a")) // a is now the most recent | 44 | require.True(t, c.has("a")) // a is now the most recent | ||
| 41 | - | c.store("c", blob(25)) // 100 fit | 45 | + | c.store("c", blob(100)) // four fit |
| 42 | - | c.store("d", blob(10)) // 110 > 100: b, the least recent, goes | 46 | + | c.store("d", blob(10)) // one more: b, the least recent, goes |
| 43 | assert.False(t, c.has("b"), "b was evicted") | 47 | assert.False(t, c.has("b"), "b was evicted") | ||
| 44 | assert.True(t, c.has("a") && c.has("c") && c.has("d") && c.has("e")) | 48 | assert.True(t, c.has("a") && c.has("c") && c.has("d") && c.has("e")) | ||
| 45 | - | assert.Equal(t, 85, c.size) | 49 | + | assert.Equal(t, 3*entry(100)+entry(10), c.size) |
| 46 | 50 | ||||
| 47 | // Replacing an entry accounts for the old size. | 51 | // Replacing an entry accounts for the old size. | ||
| 48 | c.store("a", blob(10)) | 52 | c.store("a", blob(10)) | ||
| 49 | - | assert.Equal(t, 70, c.size) | 53 | + | assert.Equal(t, 2*entry(100)+2*entry(10), c.size) |
| 50 | 54 | ||||
| 51 | // A big one pushes out as many as needed. | 55 | // A big one pushes out as many as needed. | ||
| 52 | - | c.store("f", blob(25)) | 56 | + | c.store("f", blob(100)) |
| 53 | - | c.store("g", blob(25)) | 57 | + | c.store("g", blob(100)) |
| 54 | - | assert.LessOrEqual(t, c.size, 100) | 58 | + | assert.LessOrEqual(t, c.size, budget) |
| 55 | assert.True(t, c.has("g")) | 59 | assert.True(t, c.has("g")) | ||
| 56 | } | 60 | } | ||
| 57 | 61 | ||||
| 62 | + | // Tiny bodies (a cached "no license") count their id and overhead too, so | |||
| 63 | + | // their number stays bounded by the budget. | |||
| 64 | + | func TestPathCacheBoundsTinyEntries(t *testing.T) { | |||
| 65 | + | t.Parallel() | |||
| 66 | + | c := newPathCache(1<<12, 1) | |||
| 67 | + | for i := range 10_000 { | |||
| 68 | + | c.store(fmt.Sprintf("repo\x00%040d", i), []byte("null")) | |||
| 69 | + | } | |||
| 70 | + | assert.LessOrEqual(t, c.size, 1<<12) | |||
| 71 | + | assert.Less(t, len(c.byID), (1<<12)/entryOverhead+1) | |||
| 72 | + | assert.Equal(t, len(c.byID), c.lru.Len()) | |||
| 73 | + | } | |||
| 74 | + | ||||
| 58 | // A list over a quarter of the budget is not kept, since it would push out the | 75 | // A list over a quarter of the budget is not kept, since it would push out the | ||
| 59 | // others; one at the limit is. | 76 | // others; one at the limit is. | ||
| 60 | func TestPathCacheSkipsOversizedEntries(t *testing.T) { | 77 | func TestPathCacheSkipsOversizedEntries(t *testing.T) { | ||
| 61 | t.Parallel() | 78 | t.Parallel() | ||
| 62 | - | c := newPathCache(100, 1) | 79 | + | c := newPathCache(1000, 1) |
| 63 | var builds atomic.Int32 | 80 | var builds atomic.Int32 | ||
| 64 | big := func(n int) func() ([]byte, error) { | 81 | big := func(n int) func() ([]byte, error) { | ||
| 65 | return func() ([]byte, error) { builds.Add(1); return make([]byte, n), nil } | 82 | return func() ([]byte, error) { builds.Add(1); return make([]byte, n), nil } | ||
| 66 | } | 83 | } | ||
| 67 | for range 2 { | 84 | for range 2 { | ||
| 68 | - | b, err := c.load(context.Background(), "r", "big", big(26)) | 85 | + | b, err := c.load(context.Background(), "r", "big", big(251)) |
| 69 | require.NoError(t, err) | 86 | require.NoError(t, err) | ||
| 70 | - | assert.Len(t, b, 26, "the caller still gets the list") | 87 | + | assert.Len(t, b, 251, "the caller still gets the list") |
| 71 | } | 88 | } | ||
| 72 | assert.EqualValues(t, 2, builds.Load(), "not cached, so built again") | 89 | assert.EqualValues(t, 2, builds.Load(), "not cached, so built again") | ||
| 73 | assert.False(t, c.has("big")) | 90 | assert.False(t, c.has("big")) | ||
| 74 | assert.Zero(t, c.size) | 91 | assert.Zero(t, c.size) | ||
| 75 | 92 | ||||
| 76 | for range 2 { | 93 | for range 2 { | ||
| 77 | - | _, err := c.load(context.Background(), "r", "fits", big(25)) | 94 | + | _, err := c.load(context.Background(), "r", "fits", big(250)) |
| 78 | require.NoError(t, err) | 95 | require.NoError(t, err) | ||
| 79 | } | 96 | } | ||
| 80 | assert.EqualValues(t, 3, builds.Load(), "at a quarter: kept") | 97 | assert.EqualValues(t, 3, builds.Load(), "at a quarter: kept") | ||
| 81 | - | assert.Equal(t, 25, c.size) | 98 | + | assert.Equal(t, len("fits")+250+entryOverhead, c.size) |
| 82 | } | 99 | } | ||
| 83 | 100 | ||||
| 84 | func TestPathCacheConcurrent(t *testing.T) { | 101 | func TestPathCacheConcurrent(t *testing.T) { | ||
| @@ -99,7 +116,7 @@ func TestPathCacheConcurrent(t *testing.T) { | |||||
| 99 | assert.LessOrEqual(t, c.size, 1000) | 116 | assert.LessOrEqual(t, c.size, 1000) | ||
| 100 | sum := 0 | 117 | sum := 0 | ||
| 101 | for _, el := range c.byID { | 118 | for _, el := range c.byID { | ||
| 102 | - | sum += len(el.Value.(*pathEntry).body) | 119 | + | sum += el.Value.(*pathEntry).cost() |
| 103 | } | 120 | } | ||
| 104 | assert.Equal(t, sum, c.size, "the size is the sum of the entries") | 121 | assert.Equal(t, sum, c.size, "the size is the sum of the entries") | ||
| 105 | assert.Equal(t, len(c.byID), c.lru.Len()) | 122 | assert.Equal(t, len(c.byID), c.lru.Len()) | ||
M internal/adapters/web/static/css/00-tokens.css +1 -0
| @@ -78,6 +78,7 @@ | |||
| 78 | 78 | --fs-code: 13px; | |
| 79 | 79 | --fs-s: 13px; | |
| 80 | 80 | --fs-xs: 12px; | |
| 81 | + | --fs-2xs: 11px; | |
| 81 | 82 | --lh: 24px; | |
| 82 | 83 | --lh-code: 20px; | |
| 83 | 84 | ||
| @@ -78,6 +78,7 @@ | |||||
| 78 | --fs-code: 13px; | 78 | --fs-code: 13px; | ||
| 79 | --fs-s: 13px; | 79 | --fs-s: 13px; | ||
| 80 | --fs-xs: 12px; | 80 | --fs-xs: 12px; | ||
| 81 | + | --fs-2xs: 11px; | |||
| 81 | --lh: 24px; | 82 | --lh: 24px; | ||
| 82 | --lh-code: 20px; | 83 | --lh-code: 20px; | ||
| 83 | 84 | ||||
M internal/adapters/web/static/css/20-shell.css +6 -2
| @@ -29,15 +29,19 @@ | |||
| 29 | 29 | .i-cards { --i: url(icons/cards.svg); } | |
| 30 | 30 | .i-split { --i: url(icons/split.svg); } | |
| 31 | 31 | .i-compact { --i: url(icons/compact.svg); } | |
| 32 | + | .i-refresh-cw { --i: url(icons/refresh-cw.svg); } | |
| 32 | 33 | .i-mailbox { --i: url(icons/mailbox.svg); } | |
| 33 | 34 | .i-sun-moon { --i: url(icons/sun-moon.svg); } | |
| 34 | 35 | .i-sun { --i: url(icons/sun.svg); } | |
| 35 | 36 | .i-moon { --i: url(icons/moon.svg); } | |
| 36 | 37 | ||
| 37 | 38 | .pill { | |
| 38 | - | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | |
| 39 | - | font: 600 var(--fs-xs) / 1 var(--mono); color: var(--ink-dim); vertical-align: 1px; | |
| 39 | + | display: inline-flex; align-items: center; height: 18px; padding: 0 var(--s2); margin-left: var(--s1); | |
| 40 | + | font: 600 var(--fs-2xs) / 1 var(--mono); color: var(--ink-dim); vertical-align: 1px; | |
| 40 | 41 | background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: 999px; white-space: nowrap; | |
| 42 | + | /* a link pill: no underline; the border lightens on hover and focus */ | |
| 43 | + | &:is(a) { text-decoration: none; cursor: pointer; transition: border-color .12s; } | |
| 44 | + | &:is(a):hover, &:is(a):focus-visible { text-decoration: none; border-color: var(--ink-dim); } | |
| 41 | 45 | &.tag::before { content: ""; width: 6px; height: 6px; margin-right: 6px; border: 1.5px solid currentColor; border-radius: 1px; transform: rotate(45deg); } | |
| 42 | 46 | &.new { color: var(--teal); border-color: currentColor; } | |
| 43 | 47 | /* the signature of a commit: the protocol, then the verdict, always as text | |
| @@ -29,15 +29,19 @@ | |||||
| 29 | .i-cards { --i: url(icons/cards.svg); } | 29 | .i-cards { --i: url(icons/cards.svg); } | ||
| 30 | .i-split { --i: url(icons/split.svg); } | 30 | .i-split { --i: url(icons/split.svg); } | ||
| 31 | .i-compact { --i: url(icons/compact.svg); } | 31 | .i-compact { --i: url(icons/compact.svg); } | ||
| 32 | + | .i-refresh-cw { --i: url(icons/refresh-cw.svg); } | |||
| 32 | .i-mailbox { --i: url(icons/mailbox.svg); } | 33 | .i-mailbox { --i: url(icons/mailbox.svg); } | ||
| 33 | .i-sun-moon { --i: url(icons/sun-moon.svg); } | 34 | .i-sun-moon { --i: url(icons/sun-moon.svg); } | ||
| 34 | .i-sun { --i: url(icons/sun.svg); } | 35 | .i-sun { --i: url(icons/sun.svg); } | ||
| 35 | .i-moon { --i: url(icons/moon.svg); } | 36 | .i-moon { --i: url(icons/moon.svg); } | ||
| 36 | 37 | ||||
| 37 | .pill { | 38 | .pill { | ||
| 38 | - | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | 39 | + | display: inline-flex; align-items: center; height: 18px; padding: 0 var(--s2); margin-left: var(--s1); |
| 39 | - | font: 600 var(--fs-xs) / 1 var(--mono); color: var(--ink-dim); vertical-align: 1px; | 40 | + | font: 600 var(--fs-2xs) / 1 var(--mono); color: var(--ink-dim); vertical-align: 1px; |
| 40 | background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: 999px; white-space: nowrap; | 41 | background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: 999px; white-space: nowrap; | ||
| 42 | + | /* a link pill: no underline; the border lightens on hover and focus */ | |||
| 43 | + | &:is(a) { text-decoration: none; cursor: pointer; transition: border-color .12s; } | |||
| 44 | + | &:is(a):hover, &:is(a):focus-visible { text-decoration: none; border-color: var(--ink-dim); } | |||
| 41 | &.tag::before { content: ""; width: 6px; height: 6px; margin-right: 6px; border: 1.5px solid currentColor; border-radius: 1px; transform: rotate(45deg); } | 45 | &.tag::before { content: ""; width: 6px; height: 6px; margin-right: 6px; border: 1.5px solid currentColor; border-radius: 1px; transform: rotate(45deg); } | ||
| 42 | &.new { color: var(--teal); border-color: currentColor; } | 46 | &.new { color: var(--teal); border-color: currentColor; } | ||
| 43 | /* the signature of a commit: the protocol, then the verdict, always as text | 47 | /* the signature of a commit: the protocol, then the verdict, always as text | ||
M internal/adapters/web/static/css/30-list.css +10 -10
| @@ -69,8 +69,12 @@ | |||
| 69 | 69 | & .repo-main { grid-area: main; display: flex; flex-direction: column; gap: var(--s2); min-width: 0; } | |
| 70 | 70 | & .repo-name { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s1) var(--s2); font-family: var(--mono); font-size: 17px; line-height: 26px; overflow-wrap: anywhere; } | |
| 71 | 71 | & .repo-name a { color: var(--ink-hi); text-decoration: none; } | |
| 72 | - | & .repo:hover .repo-name a, & .repo[data-cursor] .repo-name a { text-decoration: underline; } | |
| 72 | + | & .repo:hover .repo-name a:not(.pill), & .repo[data-cursor] .repo-name a:not(.pill) { text-decoration: underline; } | |
| 73 | 73 | & .repo-name .pill { margin-left: 0; } | |
| 74 | + | & .repo-name a.pill { display: block; line-height: 18px; min-width: 0; max-width: 18ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: normal; } | |
| 75 | + | & .repo-name .repo-release { color: var(--accent); } | |
| 76 | + | & .repo-name .repo-license { color: var(--ink-dim); } | |
| 77 | + | & .repo-activity { display: inline-flex; align-items: center; flex: none; color: var(--accent); } | |
| 74 | 78 | /* Every card has the same height: two lines of description, one of topics | |
| 75 | 79 | (reserved when there are none), five of commits. */ | |
| 76 | 80 | & .repo-desc { | |
| @@ -123,7 +127,7 @@ | |||
| 123 | 127 | & .repo-stats dd a { color: inherit; text-decoration: none; } | |
| 124 | 128 | & .repo-stats dd a:hover { text-decoration: underline; } | |
| 125 | 129 | & .repo-ci { | |
| 126 | - | flex: none; display: inline-flex; align-items: center; gap: var(--s1); padding-top: 2px; | |
| 130 | + | flex: none; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; | |
| 127 | 131 | font-size: var(--fs-s); color: var(--ink-dim); text-decoration: none; white-space: nowrap; | |
| 128 | 132 | } | |
| 129 | 133 | & .repo-ci:hover { color: var(--ink-hi); text-decoration: underline; } | |
| @@ -170,7 +174,7 @@ | |||
| 170 | 174 | } | |
| 171 | 175 | & .repo-main, & .repo-head { display: contents; } | |
| 172 | 176 | & .repo-name { grid-area: name; align-self: center; font-size: 16px; } | |
| 173 | - | & .repo-ci { grid-area: ci; align-self: center; padding-top: 0; } | |
| 177 | + | & .repo-ci { grid-area: ci; align-self: center; } | |
| 174 | 178 | & .repo-desc { grid-area: desc; margin-top: var(--s2); } | |
| 175 | 179 | & .repo-topics { grid-area: topics; margin-top: var(--s2); } | |
| 176 | 180 | & .repo-topics:empty { display: none; } | |
| @@ -212,7 +216,8 @@ | |||
| 212 | 216 | /* min-width 0: a long name is cut, it does not widen the column past its cap */ | |
| 213 | 217 | & .repo-name { grid-area: name; min-width: 0; flex-wrap: nowrap; gap: var(--s2); font-size: var(--fs); line-height: 24px; overflow-wrap: normal; } | |
| 214 | 218 | & .repo-name a { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 215 | - | & .repo-name .pill { flex: none; } | |
| 219 | + | & .repo-name .pill { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; } | |
| 220 | + | & .repo-name a.pill { max-width: 12ch; } | |
| 216 | 221 | & .repo-desc { | |
| 217 | 222 | grid-area: desc; display: block; min-height: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; | |
| 218 | 223 | font-size: var(--fs-s); color: var(--ink-mute); | |
| @@ -227,13 +232,8 @@ | |||
| 227 | 232 | position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); | |
| 228 | 233 | } | |
| 229 | 234 | & .repo-stats .rs-tag dd { max-width: 10ch; } | |
| 230 | - | & .repo-stats .rs-tag dd::before { | |
| 231 | - | content: ""; display: inline-block; width: 12px; height: 12px; margin-right: 3px; vertical-align: -2px; | |
| 232 | - | background: currentColor; mask: url(icons/tag.svg) center / contain no-repeat; | |
| 233 | - | } | |
| 234 | 235 | /* the CI state is its mark; the word is in the tooltip and for screen readers */ | |
| 235 | - | & .repo-ci { grid-area: ci; justify-self: center; padding-top: 0; } | |
| 236 | - | & .repo-ci .ci-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } | |
| 236 | + | & .repo-ci { grid-area: ci; justify-self: center; } | |
| 237 | 237 | /* the pulse: the same curve, small, with a bigger dot to stay seen */ | |
| 238 | 238 | & .repo-pulse { grid-area: pulse; width: 100%; height: 100%; min-height: 18px; margin: 0; padding: 0; } | |
| 239 | 239 | & div.repo-pulse { aspect-ratio: auto; box-sizing: border-box; } | |
| @@ -69,8 +69,12 @@ | |||||
| 69 | & .repo-main { grid-area: main; display: flex; flex-direction: column; gap: var(--s2); min-width: 0; } | 69 | & .repo-main { grid-area: main; display: flex; flex-direction: column; gap: var(--s2); min-width: 0; } | ||
| 70 | & .repo-name { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s1) var(--s2); font-family: var(--mono); font-size: 17px; line-height: 26px; overflow-wrap: anywhere; } | 70 | & .repo-name { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s1) var(--s2); font-family: var(--mono); font-size: 17px; line-height: 26px; overflow-wrap: anywhere; } | ||
| 71 | & .repo-name a { color: var(--ink-hi); text-decoration: none; } | 71 | & .repo-name a { color: var(--ink-hi); text-decoration: none; } | ||
| 72 | - | & .repo:hover .repo-name a, & .repo[data-cursor] .repo-name a { text-decoration: underline; } | 72 | + | & .repo:hover .repo-name a:not(.pill), & .repo[data-cursor] .repo-name a:not(.pill) { text-decoration: underline; } |
| 73 | & .repo-name .pill { margin-left: 0; } | 73 | & .repo-name .pill { margin-left: 0; } | ||
| 74 | + | & .repo-name a.pill { display: block; line-height: 18px; min-width: 0; max-width: 18ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: normal; } | |||
| 75 | + | & .repo-name .repo-release { color: var(--accent); } | |||
| 76 | + | & .repo-name .repo-license { color: var(--ink-dim); } | |||
| 77 | + | & .repo-activity { display: inline-flex; align-items: center; flex: none; color: var(--accent); } | |||
| 74 | /* Every card has the same height: two lines of description, one of topics | 78 | /* Every card has the same height: two lines of description, one of topics | ||
| 75 | (reserved when there are none), five of commits. */ | 79 | (reserved when there are none), five of commits. */ | ||
| 76 | & .repo-desc { | 80 | & .repo-desc { | ||
| @@ -123,7 +127,7 @@ | |||||
| 123 | & .repo-stats dd a { color: inherit; text-decoration: none; } | 127 | & .repo-stats dd a { color: inherit; text-decoration: none; } | ||
| 124 | & .repo-stats dd a:hover { text-decoration: underline; } | 128 | & .repo-stats dd a:hover { text-decoration: underline; } | ||
| 125 | & .repo-ci { | 129 | & .repo-ci { | ||
| 126 | - | flex: none; display: inline-flex; align-items: center; gap: var(--s1); padding-top: 2px; | 130 | + | flex: none; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; |
| 127 | font-size: var(--fs-s); color: var(--ink-dim); text-decoration: none; white-space: nowrap; | 131 | font-size: var(--fs-s); color: var(--ink-dim); text-decoration: none; white-space: nowrap; | ||
| 128 | } | 132 | } | ||
| 129 | & .repo-ci:hover { color: var(--ink-hi); text-decoration: underline; } | 133 | & .repo-ci:hover { color: var(--ink-hi); text-decoration: underline; } | ||
| @@ -170,7 +174,7 @@ | |||||
| 170 | } | 174 | } | ||
| 171 | & .repo-main, & .repo-head { display: contents; } | 175 | & .repo-main, & .repo-head { display: contents; } | ||
| 172 | & .repo-name { grid-area: name; align-self: center; font-size: 16px; } | 176 | & .repo-name { grid-area: name; align-self: center; font-size: 16px; } | ||
| 173 | - | & .repo-ci { grid-area: ci; align-self: center; padding-top: 0; } | 177 | + | & .repo-ci { grid-area: ci; align-self: center; } |
| 174 | & .repo-desc { grid-area: desc; margin-top: var(--s2); } | 178 | & .repo-desc { grid-area: desc; margin-top: var(--s2); } | ||
| 175 | & .repo-topics { grid-area: topics; margin-top: var(--s2); } | 179 | & .repo-topics { grid-area: topics; margin-top: var(--s2); } | ||
| 176 | & .repo-topics:empty { display: none; } | 180 | & .repo-topics:empty { display: none; } | ||
| @@ -212,7 +216,8 @@ | |||||
| 212 | /* min-width 0: a long name is cut, it does not widen the column past its cap */ | 216 | /* min-width 0: a long name is cut, it does not widen the column past its cap */ | ||
| 213 | & .repo-name { grid-area: name; min-width: 0; flex-wrap: nowrap; gap: var(--s2); font-size: var(--fs); line-height: 24px; overflow-wrap: normal; } | 217 | & .repo-name { grid-area: name; min-width: 0; flex-wrap: nowrap; gap: var(--s2); font-size: var(--fs); line-height: 24px; overflow-wrap: normal; } | ||
| 214 | & .repo-name a { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | 218 | & .repo-name a { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | ||
| 215 | - | & .repo-name .pill { flex: none; } | 219 | + | & .repo-name .pill { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; } |
| 220 | + | & .repo-name a.pill { max-width: 12ch; } | |||
| 216 | & .repo-desc { | 221 | & .repo-desc { | ||
| 217 | grid-area: desc; display: block; min-height: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; | 222 | grid-area: desc; display: block; min-height: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; | ||
| 218 | font-size: var(--fs-s); color: var(--ink-mute); | 223 | font-size: var(--fs-s); color: var(--ink-mute); | ||
| @@ -227,13 +232,8 @@ | |||||
| 227 | position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); | 232 | position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); | ||
| 228 | } | 233 | } | ||
| 229 | & .repo-stats .rs-tag dd { max-width: 10ch; } | 234 | & .repo-stats .rs-tag dd { max-width: 10ch; } | ||
| 230 | - | & .repo-stats .rs-tag dd::before { | |||
| 231 | - | content: ""; display: inline-block; width: 12px; height: 12px; margin-right: 3px; vertical-align: -2px; | |||
| 232 | - | background: currentColor; mask: url(icons/tag.svg) center / contain no-repeat; | |||
| 233 | - | } | |||
| 234 | /* the CI state is its mark; the word is in the tooltip and for screen readers */ | 235 | /* the CI state is its mark; the word is in the tooltip and for screen readers */ | ||
| 235 | - | & .repo-ci { grid-area: ci; justify-self: center; padding-top: 0; } | 236 | + | & .repo-ci { grid-area: ci; justify-self: center; } |
| 236 | - | & .repo-ci .ci-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } | |||
| 237 | /* the pulse: the same curve, small, with a bigger dot to stay seen */ | 237 | /* the pulse: the same curve, small, with a bigger dot to stay seen */ | ||
| 238 | & .repo-pulse { grid-area: pulse; width: 100%; height: 100%; min-height: 18px; margin: 0; padding: 0; } | 238 | & .repo-pulse { grid-area: pulse; width: 100%; height: 100%; min-height: 18px; margin: 0; padding: 0; } | ||
| 239 | & div.repo-pulse { aspect-ratio: auto; box-sizing: border-box; } | 239 | & div.repo-pulse { aspect-ratio: auto; box-sizing: border-box; } | ||
M internal/adapters/web/static/css/40-summary.css +5 -0
| @@ -13,6 +13,11 @@ | |||
| 13 | 13 | .list.commits .c-subject { overflow: hidden; text-overflow: ellipsis; } | |
| 14 | 14 | ||
| 15 | 15 | .readme .prose { padding: var(--s5) var(--s6) var(--s6); } | |
| 16 | + | .doc-tabs { | |
| 17 | + | display: flex; flex-wrap: wrap; gap: var(--s4); | |
| 18 | + | & a { color: var(--ink-dim); } | |
| 19 | + | & a[aria-current] { color: var(--teal); text-decoration: underline; text-underline-offset: 5px; } | |
| 20 | + | } | |
| 16 | 21 | ||
| 17 | 22 | /* rendered Markdown */ | |
| 18 | 23 | .prose { | |
| @@ -13,6 +13,11 @@ | |||||
| 13 | .list.commits .c-subject { overflow: hidden; text-overflow: ellipsis; } | 13 | .list.commits .c-subject { overflow: hidden; text-overflow: ellipsis; } | ||
| 14 | 14 | ||||
| 15 | .readme .prose { padding: var(--s5) var(--s6) var(--s6); } | 15 | .readme .prose { padding: var(--s5) var(--s6) var(--s6); } | ||
| 16 | + | .doc-tabs { | |||
| 17 | + | display: flex; flex-wrap: wrap; gap: var(--s4); | |||
| 18 | + | & a { color: var(--ink-dim); } | |||
| 19 | + | & a[aria-current] { color: var(--teal); text-decoration: underline; text-underline-offset: 5px; } | |||
| 20 | + | } | |||
| 16 | 21 | ||||
| 17 | /* rendered Markdown */ | 22 | /* rendered Markdown */ | ||
| 18 | .prose { | 23 | .prose { | ||
A internal/adapters/web/static/icons/refresh-cw.svg +3 -0
| @@ -0,0 +1,3 @@ | |||
| 1 | + | <!-- SPDX-License-Identifier: ISC --> | |
| 2 | + | <!-- Lucide 1.8: refresh-cw; https://lucide.dev/license --> | |
| 3 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg> | |
| @@ -0,0 +1,3 @@ | |||||
| 1 | + | <!-- SPDX-License-Identifier: ISC --> | |||
| 2 | + | <!-- Lucide 1.8: refresh-cw; https://lucide.dev/license --> | |||
| 3 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg> | |||
M internal/adapters/web/static/js/ermite.js +10 -1
| @@ -37,7 +37,8 @@ | |||
| 37 | 37 | flashTimer = setTimeout(() => (body().dataset.flash = ''), ms); | |
| 38 | 38 | }; | |
| 39 | 39 | ||
| 40 | - | const visibleRows = () => $$('[data-rows] > :not([hidden])'); | |
| 40 | + | // Rail tables are context, not the main row cursor. | |
| 41 | + | const visibleRows = () => $$('[data-rows] > :not([hidden])').filter((r) => !r.closest('.rail')); | |
| 41 | 42 | // setCursor puts data-cursor on el and takes it from the others. | |
| 42 | 43 | const setCursor = (el, others) => { | |
| 43 | 44 | for (const o of others) if (o !== el) o.removeAttribute('data-cursor'); | |
| @@ -1007,6 +1008,14 @@ window.ermiteLive = (() => { | |||
| 1007 | 1008 | const cursor = document.querySelector('[data-rows] > [data-cursor]'); | |
| 1008 | 1009 | const cursorKey = cursor?.id || cursor?.querySelector('a')?.getAttribute('href'); | |
| 1009 | 1010 | for (const el of [...t.content.children]) { | |
| 1011 | + | // Each fresh fragment owns its timer; an older swap cannot expire it. | |
| 1012 | + | // No animation, including for visitors who prefer reduced motion. | |
| 1013 | + | if (el.id.startsWith('repo-') && el.hasAttribute('data-activity')) { | |
| 1014 | + | setTimeout(() => { | |
| 1015 | + | el.querySelector('.repo-activity')?.remove(); | |
| 1016 | + | el.removeAttribute('data-activity'); | |
| 1017 | + | }, 60000); | |
| 1018 | + | } | |
| 1010 | 1019 | const old = el.id && document.getElementById(el.id); | |
| 1011 | 1020 | const had = old ? where(old) : null; // the focus is inside what is replaced | |
| 1012 | 1021 | if (el.hasAttribute('data-gone')) { | |
| @@ -37,7 +37,8 @@ | |||||
| 37 | flashTimer = setTimeout(() => (body().dataset.flash = ''), ms); | 37 | flashTimer = setTimeout(() => (body().dataset.flash = ''), ms); | ||
| 38 | }; | 38 | }; | ||
| 39 | 39 | ||||
| 40 | - | const visibleRows = () => $$('[data-rows] > :not([hidden])'); | 40 | + | // Rail tables are context, not the main row cursor. |
| 41 | + | const visibleRows = () => $$('[data-rows] > :not([hidden])').filter((r) => !r.closest('.rail')); | |||
| 41 | // setCursor puts data-cursor on el and takes it from the others. | 42 | // setCursor puts data-cursor on el and takes it from the others. | ||
| 42 | const setCursor = (el, others) => { | 43 | const setCursor = (el, others) => { | ||
| 43 | for (const o of others) if (o !== el) o.removeAttribute('data-cursor'); | 44 | for (const o of others) if (o !== el) o.removeAttribute('data-cursor'); | ||
| @@ -1007,6 +1008,14 @@ window.ermiteLive = (() => { | |||||
| 1007 | const cursor = document.querySelector('[data-rows] > [data-cursor]'); | 1008 | const cursor = document.querySelector('[data-rows] > [data-cursor]'); | ||
| 1008 | const cursorKey = cursor?.id || cursor?.querySelector('a')?.getAttribute('href'); | 1009 | const cursorKey = cursor?.id || cursor?.querySelector('a')?.getAttribute('href'); | ||
| 1009 | for (const el of [...t.content.children]) { | 1010 | for (const el of [...t.content.children]) { | ||
| 1011 | + | // Each fresh fragment owns its timer; an older swap cannot expire it. | |||
| 1012 | + | // No animation, including for visitors who prefer reduced motion. | |||
| 1013 | + | if (el.id.startsWith('repo-') && el.hasAttribute('data-activity')) { | |||
| 1014 | + | setTimeout(() => { | |||
| 1015 | + | el.querySelector('.repo-activity')?.remove(); | |||
| 1016 | + | el.removeAttribute('data-activity'); | |||
| 1017 | + | }, 60000); | |||
| 1018 | + | } | |||
| 1010 | const old = el.id && document.getElementById(el.id); | 1019 | const old = el.id && document.getElementById(el.id); | ||
| 1011 | const had = old ? where(old) : null; // the focus is inside what is replaced | 1020 | const had = old ? where(old) : null; // the focus is inside what is replaced | ||
| 1012 | if (el.hasAttribute('data-gone')) { | 1021 | if (el.hasAttribute('data-gone')) { | ||
M internal/adapters/web/summary.go +53 -29
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "html/template" | |
| 11 | 11 | "io" | |
| 12 | 12 | "net/http" | |
| 13 | + | "net/url" | |
| 13 | 14 | "strings" | |
| 14 | 15 | "time" | |
| 15 | 16 | ||
| @@ -59,22 +60,23 @@ func (s *Server) summary(ctx context.Context, r *http.Request) (templ.Component, | |||
| 59 | 60 | return nil, 0, err | |
| 60 | 61 | } | |
| 61 | 62 | branchesAt, tagsAt := namesAt(d.branches), namesAt(d.tags) | |
| 62 | - | for i, c := range commits.Commits { | |
| 63 | + | for _, c := range commits.Commits { | |
| 63 | 64 | id := c.ID.String() | |
| 64 | 65 | row := commitRow(d.name(), c, now) | |
| 65 | - | row.Branches, row.Tags, row.Cursor = branchesAt[id], tagsAt[id], i == 0 | |
| 66 | + | row.Branches, row.Tags = branchesAt[id], tagsAt[id] | |
| 66 | 67 | p.Recent = append(p.Recent, row) | |
| 67 | 68 | } | |
| 68 | 69 | ||
| 70 | + | p.License = s.repositoryLicense(ctx, d.repo.Name, d.head.CommitID.String()) | |
| 69 | 71 | p.Facts = s.facts(d, now) | |
| 70 | 72 | p.Latest = s.latest(ctx, d) | |
| 71 | 73 | p.Release = s.summaryRelease(ctx, d.repo.Name, now) | |
| 72 | 74 | p.Actions = s.recentActions(ctx, d.repo.Name, now) | |
| 73 | 75 | ||
| 74 | - | if rd, err := s.readme(ctx, d, ref); err != nil { | |
| 76 | + | if rd, tabs, err := s.summaryDoc(ctx, d, ref, r.URL.Query().Get("doc")); err != nil { | |
| 75 | 77 | return nil, 0, err | |
| 76 | 78 | } else if rd != nil { | |
| 77 | - | p.Readme, p.Outline = rd.readme, rd.outline | |
| 79 | + | p.Readme, p.Outline, p.Docs = rd.readme, rd.outline, tabs | |
| 78 | 80 | } | |
| 79 | 81 | ||
| 80 | 82 | l := s.repoLayout(ctx, d, "summary") | |
| @@ -147,40 +149,62 @@ type renderedReadme struct { | |||
| 147 | 149 | outline []Heading | |
| 148 | 150 | } | |
| 149 | 151 | ||
| 150 | - | // readme finds the README at the root of the tree of the head commit, reads | |
| 151 | - | // it and renders it. It is nil when the repository has none. Rendering | |
| 152 | - | // problems degrade to plain text; only use case failures are errors. | |
| 153 | - | func (s *Server) readme(ctx context.Context, d *repoData, ref string) (*renderedReadme, error) { | |
| 152 | + | // summaryDoc renders only a listed root document; unknown selections use the first. | |
| 153 | + | func (s *Server) summaryDoc(ctx context.Context, d *repoData, ref, selected string) (*renderedReadme, []DocTab, error) { | |
| 154 | 154 | rev := d.head.CommitID.String() | |
| 155 | 155 | tree, err := s.getTree(ctx, d.repo.Name, rev, "", false) | |
| 156 | 156 | if err != nil { | |
| 157 | - | return nil, err | |
| 157 | + | return nil, nil, err | |
| 158 | + | } | |
| 159 | + | entries := summaryDocs(tree.Entries) | |
| 160 | + | if len(entries) == 0 { | |
| 161 | + | return nil, nil, nil | |
| 158 | 162 | } | |
| 159 | - | return s.readmeIn(ctx, d.repo.Name, rev, ref, "", tree.Entries) | |
| 163 | + | index := 0 | |
| 164 | + | for i, e := range entries { | |
| 165 | + | if e.Name == selected { | |
| 166 | + | index = i | |
| 167 | + | break | |
| 168 | + | } | |
| 169 | + | if strings.EqualFold(e.Name, selected) { | |
| 170 | + | index = i | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | tabs := make([]DocTab, 0, len(entries)) | |
| 174 | + | for i, e := range entries { | |
| 175 | + | tabs = append(tabs, DocTab{Name: e.Name, Href: "?doc=" + url.QueryEscape(e.Name), Current: i == index}) | |
| 176 | + | } | |
| 177 | + | rd, err := s.renderDoc(ctx, d.repo.Name, rev, ref, "", entries[index]) | |
| 178 | + | return rd, tabs, err | |
| 160 | 179 | } | |
| 161 | 180 | ||
| 162 | - | // readmeIn picks the README among the entries of the directory dir ("" for | |
| 163 | - | // the root) of the commit rev, reads it and renders it. Links in it are | |
| 164 | - | // relative to dir, on the ref ref. It is nil when the directory has none. | |
| 165 | - | // | |
| 166 | - | //hexcheck:ignore suspicious-business-logic-in-adapter picks the README among the entries of a directory to show; display only | |
| 167 | - | func (s *Server) readmeIn(ctx context.Context, repo domain.RepoName, rev, ref, dir string, entries []domain.TreeEntry) (*renderedReadme, error) { | |
| 168 | - | var best *domain.TreeEntry | |
| 169 | - | rank := len(readmeNames) | |
| 170 | - | for i := range entries { | |
| 171 | - | e := &entries[i] | |
| 172 | - | if e.Kind != domain.EntryFile && e.Kind != domain.EntryExecutable { | |
| 173 | - | continue | |
| 174 | - | } | |
| 175 | - | for i, n := range readmeNames { | |
| 176 | - | if i < rank && strings.EqualFold(e.Name, n) { | |
| 177 | - | best, rank = e, i | |
| 181 | + | // summaryDocs lists the preferred README, then the common project documents. | |
| 182 | + | func summaryDocs(entries []domain.TreeEntry) []domain.TreeEntry { | |
| 183 | + | var docs []domain.TreeEntry | |
| 184 | + | groups := [][]string{readmeNames} | |
| 185 | + | for _, base := range []string{"license", "licence", "copying", "contributing", "code_of_conduct", "security"} { | |
| 186 | + | groups = append(groups, []string{base, base + ".md", base + ".markdown", base + ".txt"}) | |
| 187 | + | } | |
| 188 | + | for g, names := range groups { | |
| 189 | + | for _, name := range names { | |
| 190 | + | for _, e := range entries { | |
| 191 | + | if (e.Kind == domain.EntryFile || e.Kind == domain.EntryExecutable) && strings.EqualFold(e.Name, name) { | |
| 192 | + | docs = append(docs, e) | |
| 193 | + | } | |
| 194 | + | } | |
| 195 | + | if g == 0 && len(docs) > 0 { | |
| 196 | + | break | |
| 178 | 197 | } | |
| 179 | 198 | } | |
| 180 | 199 | } | |
| 181 | - | if best == nil { | |
| 182 | - | return nil, nil | |
| 183 | - | } | |
| 200 | + | return docs | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // renderDoc shares the README limits and rendering with the summary documents. | |
| 204 | + | // | |
| 205 | + | //hexcheck:ignore suspicious-business-logic-in-adapter reads and renders a root document to show; display only | |
| 206 | + | func (s *Server) renderDoc(ctx context.Context, repo domain.RepoName, rev, ref, dir string, entry domain.TreeEntry) (*renderedReadme, error) { | |
| 207 | + | best := &entry | |
| 184 | 208 | rd := &Readme{Name: best.Name} | |
| 185 | 209 | if best.Size > maxReadmeBytes { | |
| 186 | 210 | rd.Note = best.Name + " is too large to show here (" + humanSize(best.Size) + ")." | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "html/template" | 10 | "html/template" | ||
| 11 | "io" | 11 | "io" | ||
| 12 | "net/http" | 12 | "net/http" | ||
| 13 | + | "net/url" | |||
| 13 | "strings" | 14 | "strings" | ||
| 14 | "time" | 15 | "time" | ||
| 15 | 16 | ||||
| @@ -59,22 +60,23 @@ func (s *Server) summary(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 59 | return nil, 0, err | 60 | return nil, 0, err | ||
| 60 | } | 61 | } | ||
| 61 | branchesAt, tagsAt := namesAt(d.branches), namesAt(d.tags) | 62 | branchesAt, tagsAt := namesAt(d.branches), namesAt(d.tags) | ||
| 62 | - | for i, c := range commits.Commits { | 63 | + | for _, c := range commits.Commits { |
| 63 | id := c.ID.String() | 64 | id := c.ID.String() | ||
| 64 | row := commitRow(d.name(), c, now) | 65 | row := commitRow(d.name(), c, now) | ||
| 65 | - | row.Branches, row.Tags, row.Cursor = branchesAt[id], tagsAt[id], i == 0 | 66 | + | row.Branches, row.Tags = branchesAt[id], tagsAt[id] |
| 66 | p.Recent = append(p.Recent, row) | 67 | p.Recent = append(p.Recent, row) | ||
| 67 | } | 68 | } | ||
| 68 | 69 | ||||
| 70 | + | p.License = s.repositoryLicense(ctx, d.repo.Name, d.head.CommitID.String()) | |||
| 69 | p.Facts = s.facts(d, now) | 71 | p.Facts = s.facts(d, now) | ||
| 70 | p.Latest = s.latest(ctx, d) | 72 | p.Latest = s.latest(ctx, d) | ||
| 71 | p.Release = s.summaryRelease(ctx, d.repo.Name, now) | 73 | p.Release = s.summaryRelease(ctx, d.repo.Name, now) | ||
| 72 | p.Actions = s.recentActions(ctx, d.repo.Name, now) | 74 | p.Actions = s.recentActions(ctx, d.repo.Name, now) | ||
| 73 | 75 | ||||
| 74 | - | if rd, err := s.readme(ctx, d, ref); err != nil { | 76 | + | if rd, tabs, err := s.summaryDoc(ctx, d, ref, r.URL.Query().Get("doc")); err != nil { |
| 75 | return nil, 0, err | 77 | return nil, 0, err | ||
| 76 | } else if rd != nil { | 78 | } else if rd != nil { | ||
| 77 | - | p.Readme, p.Outline = rd.readme, rd.outline | 79 | + | p.Readme, p.Outline, p.Docs = rd.readme, rd.outline, tabs |
| 78 | } | 80 | } | ||
| 79 | 81 | ||||
| 80 | l := s.repoLayout(ctx, d, "summary") | 82 | l := s.repoLayout(ctx, d, "summary") | ||
| @@ -147,40 +149,62 @@ type renderedReadme struct { | |||||
| 147 | outline []Heading | 149 | outline []Heading | ||
| 148 | } | 150 | } | ||
| 149 | 151 | ||||
| 150 | - | // readme finds the README at the root of the tree of the head commit, reads | 152 | + | // summaryDoc renders only a listed root document; unknown selections use the first. |
| 151 | - | // it and renders it. It is nil when the repository has none. Rendering | 153 | + | func (s *Server) summaryDoc(ctx context.Context, d *repoData, ref, selected string) (*renderedReadme, []DocTab, error) { |
| 152 | - | // problems degrade to plain text; only use case failures are errors. | |||
| 153 | - | func (s *Server) readme(ctx context.Context, d *repoData, ref string) (*renderedReadme, error) { | |||
| 154 | rev := d.head.CommitID.String() | 154 | rev := d.head.CommitID.String() | ||
| 155 | tree, err := s.getTree(ctx, d.repo.Name, rev, "", false) | 155 | tree, err := s.getTree(ctx, d.repo.Name, rev, "", false) | ||
| 156 | if err != nil { | 156 | if err != nil { | ||
| 157 | - | return nil, err | 157 | + | return nil, nil, err |
| 158 | + | } | |||
| 159 | + | entries := summaryDocs(tree.Entries) | |||
| 160 | + | if len(entries) == 0 { | |||
| 161 | + | return nil, nil, nil | |||
| 158 | } | 162 | } | ||
| 159 | - | return s.readmeIn(ctx, d.repo.Name, rev, ref, "", tree.Entries) | 163 | + | index := 0 |
| 164 | + | for i, e := range entries { | |||
| 165 | + | if e.Name == selected { | |||
| 166 | + | index = i | |||
| 167 | + | break | |||
| 168 | + | } | |||
| 169 | + | if strings.EqualFold(e.Name, selected) { | |||
| 170 | + | index = i | |||
| 171 | + | } | |||
| 172 | + | } | |||
| 173 | + | tabs := make([]DocTab, 0, len(entries)) | |||
| 174 | + | for i, e := range entries { | |||
| 175 | + | tabs = append(tabs, DocTab{Name: e.Name, Href: "?doc=" + url.QueryEscape(e.Name), Current: i == index}) | |||
| 176 | + | } | |||
| 177 | + | rd, err := s.renderDoc(ctx, d.repo.Name, rev, ref, "", entries[index]) | |||
| 178 | + | return rd, tabs, err | |||
| 160 | } | 179 | } | ||
| 161 | 180 | ||||
| 162 | - | // readmeIn picks the README among the entries of the directory dir ("" for | 181 | + | // summaryDocs lists the preferred README, then the common project documents. |
| 163 | - | // the root) of the commit rev, reads it and renders it. Links in it are | 182 | + | func summaryDocs(entries []domain.TreeEntry) []domain.TreeEntry { |
| 164 | - | // relative to dir, on the ref ref. It is nil when the directory has none. | 183 | + | var docs []domain.TreeEntry |
| 165 | - | // | 184 | + | groups := [][]string{readmeNames} |
| 166 | - | //hexcheck:ignore suspicious-business-logic-in-adapter picks the README among the entries of a directory to show; display only | 185 | + | for _, base := range []string{"license", "licence", "copying", "contributing", "code_of_conduct", "security"} { |
| 167 | - | func (s *Server) readmeIn(ctx context.Context, repo domain.RepoName, rev, ref, dir string, entries []domain.TreeEntry) (*renderedReadme, error) { | 186 | + | groups = append(groups, []string{base, base + ".md", base + ".markdown", base + ".txt"}) |
| 168 | - | var best *domain.TreeEntry | 187 | + | } |
| 169 | - | rank := len(readmeNames) | 188 | + | for g, names := range groups { |
| 170 | - | for i := range entries { | 189 | + | for _, name := range names { |
| 171 | - | e := &entries[i] | 190 | + | for _, e := range entries { |
| 172 | - | if e.Kind != domain.EntryFile && e.Kind != domain.EntryExecutable { | 191 | + | if (e.Kind == domain.EntryFile || e.Kind == domain.EntryExecutable) && strings.EqualFold(e.Name, name) { |
| 173 | - | continue | 192 | + | docs = append(docs, e) |
| 174 | - | } | 193 | + | } |
| 175 | - | for i, n := range readmeNames { | 194 | + | } |
| 176 | - | if i < rank && strings.EqualFold(e.Name, n) { | 195 | + | if g == 0 && len(docs) > 0 { |
| 177 | - | best, rank = e, i | 196 | + | break |
| 178 | } | 197 | } | ||
| 179 | } | 198 | } | ||
| 180 | } | 199 | } | ||
| 181 | - | if best == nil { | 200 | + | return docs |
| 182 | - | return nil, nil | 201 | + | } |
| 183 | - | } | 202 | + | |
| 203 | + | // renderDoc shares the README limits and rendering with the summary documents. | |||
| 204 | + | // | |||
| 205 | + | //hexcheck:ignore suspicious-business-logic-in-adapter reads and renders a root document to show; display only | |||
| 206 | + | func (s *Server) renderDoc(ctx context.Context, repo domain.RepoName, rev, ref, dir string, entry domain.TreeEntry) (*renderedReadme, error) { | |||
| 207 | + | best := &entry | |||
| 184 | rd := &Readme{Name: best.Name} | 208 | rd := &Readme{Name: best.Name} | ||
| 185 | if best.Size > maxReadmeBytes { | 209 | if best.Size > maxReadmeBytes { | ||
| 186 | rd.Note = best.Name + " is too large to show here (" + humanSize(best.Size) + ")." | 210 | rd.Note = best.Name + " is too large to show here (" + humanSize(best.Size) + ")." | ||
A internal/adapters/web/summary_docs_test.go +157 -0
| @@ -0,0 +1,157 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "io" | |
| 8 | + | "net/http" | |
| 9 | + | "net/http/httptest" | |
| 10 | + | "strings" | |
| 11 | + | "testing" | |
| 12 | + | ||
| 13 | + | "github.com/stretchr/testify/assert" | |
| 14 | + | "github.com/stretchr/testify/mock" | |
| 15 | + | "github.com/stretchr/testify/require" | |
| 16 | + | ||
| 17 | + | "bnema.dev/ermite/internal/domain" | |
| 18 | + | "bnema.dev/ermite/internal/ports" | |
| 19 | + | "bnema.dev/ermite/internal/ports/mocks" | |
| 20 | + | ) | |
| 21 | + | ||
| 22 | + | func TestSummaryDocs(t *testing.T) { | |
| 23 | + | t.Parallel() | |
| 24 | + | entries := []domain.TreeEntry{ | |
| 25 | + | {Name: "readme.txt", Kind: domain.EntryFile}, | |
| 26 | + | {Name: "SECURITY.md", Kind: domain.EntryFile}, | |
| 27 | + | {Name: "LICENSE", Kind: domain.EntryExecutable}, | |
| 28 | + | {Name: "README.md", Kind: domain.EntryFile}, | |
| 29 | + | {Name: "CONTRIBUTING.md", Kind: domain.EntryDirectory}, | |
| 30 | + | {Name: "COPYING.txt", Kind: domain.EntryFile}, | |
| 31 | + | {Name: "code_of_conduct.MD", Kind: domain.EntryFile}, | |
| 32 | + | } | |
| 33 | + | docs := summaryDocs(entries) | |
| 34 | + | var names []string | |
| 35 | + | for _, e := range docs { | |
| 36 | + | names = append(names, e.Name) | |
| 37 | + | } | |
| 38 | + | assert.Equal(t, []string{"README.md", "LICENSE", "COPYING.txt", "code_of_conduct.MD", "SECURITY.md"}, names) | |
| 39 | + | assert.Empty(t, summaryDocs(nil)) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func TestSummaryDocSelection(t *testing.T) { | |
| 43 | + | t.Parallel() | |
| 44 | + | for _, tc := range []struct { | |
| 45 | + | selected, want, body string | |
| 46 | + | markdown, noReadme bool | |
| 47 | + | }{ | |
| 48 | + | {"", "README.md", "# Read me", true, false}, | |
| 49 | + | {"security.MD", "SECURITY.md", "# Reporting", true, false}, | |
| 50 | + | {"LICENSE", "LICENSE", "<license>", false, false}, | |
| 51 | + | {"../secret", "README.md", "# Read me", true, false}, | |
| 52 | + | {"", "LICENSE", "<license>", false, true}, | |
| 53 | + | } { | |
| 54 | + | t.Run(tc.selected+tc.want, func(t *testing.T) { | |
| 55 | + | t.Parallel() | |
| 56 | + | repo := repoName(t, "r") | |
| 57 | + | browse := mocks.NewBrowseService(t) | |
| 58 | + | entries := []domain.TreeEntry{{Name: "LICENSE", Path: "LICENSE", Kind: domain.EntryFile}, {Name: "SECURITY.md", Path: "SECURITY.md", Kind: domain.EntryFile}} | |
| 59 | + | if !tc.noReadme { | |
| 60 | + | entries = append(entries, domain.TreeEntry{Name: "README.md", Path: "README.md", Kind: domain.EntryFile}) | |
| 61 | + | } | |
| 62 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{Entries: entries}, nil) | |
| 63 | + | browse.EXPECT().OpenBlob(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, _ domain.Principal, _ domain.RepoName, _ domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error) { | |
| 64 | + | assert.Equal(t, tc.want, path.String()) | |
| 65 | + | return domain.BlobInfo{}, io.NopCloser(strings.NewReader(tc.body)), nil | |
| 66 | + | }) | |
| 67 | + | s := newTestServer(t, Config{Repos: mocks.NewRepoService(t), Browse: browse}) | |
| 68 | + | d := &repoData{repo: ports.RepoView{Name: repo}, head: &domain.Ref{CommitID: objectID(t, mainTip)}} | |
| 69 | + | rd, tabs, err := s.summaryDoc(t.Context(), d, "main", tc.selected) | |
| 70 | + | require.NoError(t, err) | |
| 71 | + | require.NotNil(t, rd) | |
| 72 | + | assert.Equal(t, tc.want, rd.readme.Name) | |
| 73 | + | if tc.markdown { | |
| 74 | + | assert.Contains(t, string(rd.readme.HTML), "<h1") | |
| 75 | + | assert.Len(t, rd.outline, 1) | |
| 76 | + | } else { | |
| 77 | + | assert.Equal(t, "<pre><license></pre>", string(rd.readme.HTML)) | |
| 78 | + | assert.Empty(t, rd.outline) | |
| 79 | + | } | |
| 80 | + | current := 0 | |
| 81 | + | for _, tab := range tabs { | |
| 82 | + | if tab.Current { | |
| 83 | + | current++ | |
| 84 | + | assert.Equal(t, tc.want, tab.Name) | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | assert.Equal(t, 1, current) | |
| 88 | + | out := renderToString(t, ReadmeCard(*rd.readme, tabs...)) | |
| 89 | + | assert.Contains(t, out, `aria-label="Repository documents"`) | |
| 90 | + | assert.Contains(t, out, `aria-current="page"`) | |
| 91 | + | assert.Contains(t, out, `href="?doc=LICENSE"`) | |
| 92 | + | }) | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func TestSummaryDocLimits(t *testing.T) { | |
| 97 | + | t.Parallel() | |
| 98 | + | s := newTestServer(t, Config{Repos: mocks.NewRepoService(t), Browse: mocks.NewBrowseService(t)}) | |
| 99 | + | rd, err := s.renderDoc(t.Context(), repoName(t, "r"), mainTip, "main", "", domain.TreeEntry{Name: "LICENSE", Size: maxReadmeBytes + 1}) | |
| 100 | + | require.NoError(t, err) | |
| 101 | + | assert.Contains(t, rd.readme.Note, "too large") | |
| 102 | + | assert.Empty(t, rd.outline) | |
| 103 | + | out := renderToString(t, ReadmeCard(*rd.readme)) | |
| 104 | + | assert.NotContains(t, out, "doc-tabs") | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | func TestSummaryArrivalHasNoRailCursor(t *testing.T) { | |
| 108 | + | t.Parallel() | |
| 109 | + | repo := repoName(t, "r") | |
| 110 | + | browse := mocks.NewBrowseService(t) | |
| 111 | + | tip := objectID(t, mainTip) | |
| 112 | + | browse.EXPECT().ListRefs(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.RefPage{Refs: []domain.Ref{{Name: "refs/heads/main", ShortName: "main", Kind: domain.RefKindBranch, CommitID: tip}}}, nil) | |
| 113 | + | browse.EXPECT().ListCommits(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.HistoryPage{Commits: []domain.Commit{{ID: tip, Summary: "recent commit"}}}, nil) | |
| 114 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{}, nil) | |
| 115 | + | s := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "r")), Browse: browse}) | |
| 116 | + | req := httptest.NewRequest(http.MethodGet, "/r", nil) | |
| 117 | + | req.SetPathValue("repo", "r") | |
| 118 | + | page, status, err := s.summary(t.Context(), req) | |
| 119 | + | require.NoError(t, err) | |
| 120 | + | assert.Equal(t, http.StatusOK, status) | |
| 121 | + | out := renderToString(t, page) | |
| 122 | + | assert.Contains(t, out, "recent commit") | |
| 123 | + | _, rail, found := strings.Cut(out, `<aside class="rail"`) | |
| 124 | + | require.True(t, found) | |
| 125 | + | rail, _, _ = strings.Cut(rail, "</aside>") | |
| 126 | + | assert.NotContains(t, rail, "data-cursor") | |
| 127 | + | assert.NotContains(t, rail, "autofocus") | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | func TestSummaryDocExactFilename(t *testing.T) { | |
| 131 | + | t.Parallel() | |
| 132 | + | for _, selected := range []string{"LICENSE", "license"} { | |
| 133 | + | t.Run(selected, func(t *testing.T) { | |
| 134 | + | t.Parallel() | |
| 135 | + | repo := repoName(t, "r") | |
| 136 | + | browse := mocks.NewBrowseService(t) | |
| 137 | + | entries := []domain.TreeEntry{{Name: "LICENSE", Path: "LICENSE", Kind: domain.EntryFile}, {Name: "license", Path: "license", Kind: domain.EntryFile}} | |
| 138 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{Entries: entries}, nil) | |
| 139 | + | browse.EXPECT().OpenBlob(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, _ domain.Principal, _ domain.RepoName, _ domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error) { | |
| 140 | + | return domain.BlobInfo{}, io.NopCloser(strings.NewReader("content of " + path.String())), nil | |
| 141 | + | }) | |
| 142 | + | s := newTestServer(t, Config{Repos: mocks.NewRepoService(t), Browse: browse}) | |
| 143 | + | d := &repoData{repo: ports.RepoView{Name: repo}, head: &domain.Ref{CommitID: objectID(t, mainTip)}} | |
| 144 | + | req := httptest.NewRequest(http.MethodGet, "/r?doc="+selected, nil) | |
| 145 | + | rd, tabs, err := s.summaryDoc(t.Context(), d, "main", req.URL.Query().Get("doc")) | |
| 146 | + | require.NoError(t, err) | |
| 147 | + | require.NotNil(t, rd) | |
| 148 | + | assert.Equal(t, selected, rd.readme.Name) | |
| 149 | + | assert.Equal(t, "<pre>content of "+selected+"</pre>", string(rd.readme.HTML)) | |
| 150 | + | require.Len(t, tabs, 2) | |
| 151 | + | for _, tab := range tabs { | |
| 152 | + | assert.Equal(t, tab.Name == selected, tab.Current, tab.Name) | |
| 153 | + | assert.Equal(t, "?doc="+tab.Name, tab.Href) | |
| 154 | + | } | |
| 155 | + | }) | |
| 156 | + | } | |
| 157 | + | } | |
| @@ -0,0 +1,157 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package web | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "io" | |||
| 8 | + | "net/http" | |||
| 9 | + | "net/http/httptest" | |||
| 10 | + | "strings" | |||
| 11 | + | "testing" | |||
| 12 | + | ||||
| 13 | + | "github.com/stretchr/testify/assert" | |||
| 14 | + | "github.com/stretchr/testify/mock" | |||
| 15 | + | "github.com/stretchr/testify/require" | |||
| 16 | + | ||||
| 17 | + | "bnema.dev/ermite/internal/domain" | |||
| 18 | + | "bnema.dev/ermite/internal/ports" | |||
| 19 | + | "bnema.dev/ermite/internal/ports/mocks" | |||
| 20 | + | ) | |||
| 21 | + | ||||
| 22 | + | func TestSummaryDocs(t *testing.T) { | |||
| 23 | + | t.Parallel() | |||
| 24 | + | entries := []domain.TreeEntry{ | |||
| 25 | + | {Name: "readme.txt", Kind: domain.EntryFile}, | |||
| 26 | + | {Name: "SECURITY.md", Kind: domain.EntryFile}, | |||
| 27 | + | {Name: "LICENSE", Kind: domain.EntryExecutable}, | |||
| 28 | + | {Name: "README.md", Kind: domain.EntryFile}, | |||
| 29 | + | {Name: "CONTRIBUTING.md", Kind: domain.EntryDirectory}, | |||
| 30 | + | {Name: "COPYING.txt", Kind: domain.EntryFile}, | |||
| 31 | + | {Name: "code_of_conduct.MD", Kind: domain.EntryFile}, | |||
| 32 | + | } | |||
| 33 | + | docs := summaryDocs(entries) | |||
| 34 | + | var names []string | |||
| 35 | + | for _, e := range docs { | |||
| 36 | + | names = append(names, e.Name) | |||
| 37 | + | } | |||
| 38 | + | assert.Equal(t, []string{"README.md", "LICENSE", "COPYING.txt", "code_of_conduct.MD", "SECURITY.md"}, names) | |||
| 39 | + | assert.Empty(t, summaryDocs(nil)) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func TestSummaryDocSelection(t *testing.T) { | |||
| 43 | + | t.Parallel() | |||
| 44 | + | for _, tc := range []struct { | |||
| 45 | + | selected, want, body string | |||
| 46 | + | markdown, noReadme bool | |||
| 47 | + | }{ | |||
| 48 | + | {"", "README.md", "# Read me", true, false}, | |||
| 49 | + | {"security.MD", "SECURITY.md", "# Reporting", true, false}, | |||
| 50 | + | {"LICENSE", "LICENSE", "<license>", false, false}, | |||
| 51 | + | {"../secret", "README.md", "# Read me", true, false}, | |||
| 52 | + | {"", "LICENSE", "<license>", false, true}, | |||
| 53 | + | } { | |||
| 54 | + | t.Run(tc.selected+tc.want, func(t *testing.T) { | |||
| 55 | + | t.Parallel() | |||
| 56 | + | repo := repoName(t, "r") | |||
| 57 | + | browse := mocks.NewBrowseService(t) | |||
| 58 | + | entries := []domain.TreeEntry{{Name: "LICENSE", Path: "LICENSE", Kind: domain.EntryFile}, {Name: "SECURITY.md", Path: "SECURITY.md", Kind: domain.EntryFile}} | |||
| 59 | + | if !tc.noReadme { | |||
| 60 | + | entries = append(entries, domain.TreeEntry{Name: "README.md", Path: "README.md", Kind: domain.EntryFile}) | |||
| 61 | + | } | |||
| 62 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{Entries: entries}, nil) | |||
| 63 | + | browse.EXPECT().OpenBlob(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, _ domain.Principal, _ domain.RepoName, _ domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error) { | |||
| 64 | + | assert.Equal(t, tc.want, path.String()) | |||
| 65 | + | return domain.BlobInfo{}, io.NopCloser(strings.NewReader(tc.body)), nil | |||
| 66 | + | }) | |||
| 67 | + | s := newTestServer(t, Config{Repos: mocks.NewRepoService(t), Browse: browse}) | |||
| 68 | + | d := &repoData{repo: ports.RepoView{Name: repo}, head: &domain.Ref{CommitID: objectID(t, mainTip)}} | |||
| 69 | + | rd, tabs, err := s.summaryDoc(t.Context(), d, "main", tc.selected) | |||
| 70 | + | require.NoError(t, err) | |||
| 71 | + | require.NotNil(t, rd) | |||
| 72 | + | assert.Equal(t, tc.want, rd.readme.Name) | |||
| 73 | + | if tc.markdown { | |||
| 74 | + | assert.Contains(t, string(rd.readme.HTML), "<h1") | |||
| 75 | + | assert.Len(t, rd.outline, 1) | |||
| 76 | + | } else { | |||
| 77 | + | assert.Equal(t, "<pre><license></pre>", string(rd.readme.HTML)) | |||
| 78 | + | assert.Empty(t, rd.outline) | |||
| 79 | + | } | |||
| 80 | + | current := 0 | |||
| 81 | + | for _, tab := range tabs { | |||
| 82 | + | if tab.Current { | |||
| 83 | + | current++ | |||
| 84 | + | assert.Equal(t, tc.want, tab.Name) | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | assert.Equal(t, 1, current) | |||
| 88 | + | out := renderToString(t, ReadmeCard(*rd.readme, tabs...)) | |||
| 89 | + | assert.Contains(t, out, `aria-label="Repository documents"`) | |||
| 90 | + | assert.Contains(t, out, `aria-current="page"`) | |||
| 91 | + | assert.Contains(t, out, `href="?doc=LICENSE"`) | |||
| 92 | + | }) | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func TestSummaryDocLimits(t *testing.T) { | |||
| 97 | + | t.Parallel() | |||
| 98 | + | s := newTestServer(t, Config{Repos: mocks.NewRepoService(t), Browse: mocks.NewBrowseService(t)}) | |||
| 99 | + | rd, err := s.renderDoc(t.Context(), repoName(t, "r"), mainTip, "main", "", domain.TreeEntry{Name: "LICENSE", Size: maxReadmeBytes + 1}) | |||
| 100 | + | require.NoError(t, err) | |||
| 101 | + | assert.Contains(t, rd.readme.Note, "too large") | |||
| 102 | + | assert.Empty(t, rd.outline) | |||
| 103 | + | out := renderToString(t, ReadmeCard(*rd.readme)) | |||
| 104 | + | assert.NotContains(t, out, "doc-tabs") | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | func TestSummaryArrivalHasNoRailCursor(t *testing.T) { | |||
| 108 | + | t.Parallel() | |||
| 109 | + | repo := repoName(t, "r") | |||
| 110 | + | browse := mocks.NewBrowseService(t) | |||
| 111 | + | tip := objectID(t, mainTip) | |||
| 112 | + | browse.EXPECT().ListRefs(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.RefPage{Refs: []domain.Ref{{Name: "refs/heads/main", ShortName: "main", Kind: domain.RefKindBranch, CommitID: tip}}}, nil) | |||
| 113 | + | browse.EXPECT().ListCommits(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.HistoryPage{Commits: []domain.Commit{{ID: tip, Summary: "recent commit"}}}, nil) | |||
| 114 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{}, nil) | |||
| 115 | + | s := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "r")), Browse: browse}) | |||
| 116 | + | req := httptest.NewRequest(http.MethodGet, "/r", nil) | |||
| 117 | + | req.SetPathValue("repo", "r") | |||
| 118 | + | page, status, err := s.summary(t.Context(), req) | |||
| 119 | + | require.NoError(t, err) | |||
| 120 | + | assert.Equal(t, http.StatusOK, status) | |||
| 121 | + | out := renderToString(t, page) | |||
| 122 | + | assert.Contains(t, out, "recent commit") | |||
| 123 | + | _, rail, found := strings.Cut(out, `<aside class="rail"`) | |||
| 124 | + | require.True(t, found) | |||
| 125 | + | rail, _, _ = strings.Cut(rail, "</aside>") | |||
| 126 | + | assert.NotContains(t, rail, "data-cursor") | |||
| 127 | + | assert.NotContains(t, rail, "autofocus") | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | func TestSummaryDocExactFilename(t *testing.T) { | |||
| 131 | + | t.Parallel() | |||
| 132 | + | for _, selected := range []string{"LICENSE", "license"} { | |||
| 133 | + | t.Run(selected, func(t *testing.T) { | |||
| 134 | + | t.Parallel() | |||
| 135 | + | repo := repoName(t, "r") | |||
| 136 | + | browse := mocks.NewBrowseService(t) | |||
| 137 | + | entries := []domain.TreeEntry{{Name: "LICENSE", Path: "LICENSE", Kind: domain.EntryFile}, {Name: "license", Path: "license", Kind: domain.EntryFile}} | |||
| 138 | + | browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{Entries: entries}, nil) | |||
| 139 | + | browse.EXPECT().OpenBlob(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, _ domain.Principal, _ domain.RepoName, _ domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error) { | |||
| 140 | + | return domain.BlobInfo{}, io.NopCloser(strings.NewReader("content of " + path.String())), nil | |||
| 141 | + | }) | |||
| 142 | + | s := newTestServer(t, Config{Repos: mocks.NewRepoService(t), Browse: browse}) | |||
| 143 | + | d := &repoData{repo: ports.RepoView{Name: repo}, head: &domain.Ref{CommitID: objectID(t, mainTip)}} | |||
| 144 | + | req := httptest.NewRequest(http.MethodGet, "/r?doc="+selected, nil) | |||
| 145 | + | rd, tabs, err := s.summaryDoc(t.Context(), d, "main", req.URL.Query().Get("doc")) | |||
| 146 | + | require.NoError(t, err) | |||
| 147 | + | require.NotNil(t, rd) | |||
| 148 | + | assert.Equal(t, selected, rd.readme.Name) | |||
| 149 | + | assert.Equal(t, "<pre>content of "+selected+"</pre>", string(rd.readme.HTML)) | |||
| 150 | + | require.Len(t, tabs, 2) | |||
| 151 | + | for _, tab := range tabs { | |||
| 152 | + | assert.Equal(t, tab.Name == selected, tab.Current, tab.Name) | |||
| 153 | + | assert.Equal(t, "?doc="+tab.Name, tab.Href) | |||
| 154 | + | } | |||
| 155 | + | }) | |||
| 156 | + | } | |||
| 157 | + | } | |||
M internal/adapters/web/view.go +14 -4
| @@ -84,10 +84,12 @@ type Link struct{ Label, Href string } | |||
| 84 | 84 | ||
| 85 | 85 | // HomeRow is one repository in the home list. | |
| 86 | 86 | type HomeRow struct { | |
| 87 | - | ID string | |
| 88 | - | Name string | |
| 89 | - | Href string | |
| 90 | - | Desc string | |
| 87 | + | Release *LatestRelease | |
| 88 | + | License *LicenseBadge | |
| 89 | + | ID string | |
| 90 | + | Name string | |
| 91 | + | Href string | |
| 92 | + | Desc string | |
| 91 | 93 | // Topics classify the repository, as on GitHub. | |
| 92 | 94 | Topics []string | |
| 93 | 95 | // Active is when the repository last changed (its last push or merge), | |
| @@ -201,8 +203,16 @@ type CommitRow struct { | |||
| 201 | 203 | Sig *SignatureBadge | |
| 202 | 204 | } | |
| 203 | 205 | ||
| 206 | + | // DocTab links to a root document on the summary. | |
| 207 | + | type DocTab struct { | |
| 208 | + | Name, Href string | |
| 209 | + | Current bool | |
| 210 | + | } | |
| 211 | + | ||
| 204 | 212 | // SummaryPage is the repository summary. | |
| 205 | 213 | type SummaryPage struct { | |
| 214 | + | Docs []DocTab | |
| 215 | + | License *LicenseBadge | |
| 206 | 216 | Mirror *MirrorBadge | |
| 207 | 217 | Facts []Fact | |
| 208 | 218 | Clone Clone | |
| @@ -84,10 +84,12 @@ type Link struct{ Label, Href string } | |||||
| 84 | 84 | ||||
| 85 | // HomeRow is one repository in the home list. | 85 | // HomeRow is one repository in the home list. | ||
| 86 | type HomeRow struct { | 86 | type HomeRow struct { | ||
| 87 | - | ID string | 87 | + | Release *LatestRelease |
| 88 | - | Name string | 88 | + | License *LicenseBadge |
| 89 | - | Href string | 89 | + | ID string |
| 90 | - | Desc string | 90 | + | Name string |
| 91 | + | Href string | |||
| 92 | + | Desc string | |||
| 91 | // Topics classify the repository, as on GitHub. | 93 | // Topics classify the repository, as on GitHub. | ||
| 92 | Topics []string | 94 | Topics []string | ||
| 93 | // Active is when the repository last changed (its last push or merge), | 95 | // Active is when the repository last changed (its last push or merge), | ||
| @@ -201,8 +203,16 @@ type CommitRow struct { | |||||
| 201 | Sig *SignatureBadge | 203 | Sig *SignatureBadge | ||
| 202 | } | 204 | } | ||
| 203 | 205 | ||||
| 206 | + | // DocTab links to a root document on the summary. | |||
| 207 | + | type DocTab struct { | |||
| 208 | + | Name, Href string | |||
| 209 | + | Current bool | |||
| 210 | + | } | |||
| 211 | + | ||||
| 204 | // SummaryPage is the repository summary. | 212 | // SummaryPage is the repository summary. | ||
| 205 | type SummaryPage struct { | 213 | type SummaryPage struct { | ||
| 214 | + | Docs []DocTab | |||
| 215 | + | License *LicenseBadge | |||
| 206 | Mirror *MirrorBadge | 216 | Mirror *MirrorBadge | ||
| 207 | Facts []Fact | 217 | Facts []Fact | ||
| 208 | Clone Clone | 218 | Clone Clone | ||
M internal/adapters/web/web.go +3 -0
| @@ -139,6 +139,8 @@ type Server struct { | |||
| 139 | 139 | md *markdown | |
| 140 | 140 | // pathCache holds the path lists the file finder fetches. | |
| 141 | 141 | pathCache *pathCache | |
| 142 | + | // licenses holds small display results, including missing licenses. | |
| 143 | + | licenses *pathCache | |
| 142 | 144 | // hl holds the colours of highlighted diffs. | |
| 143 | 145 | hl *hlCache | |
| 144 | 146 | mux *http.ServeMux | |
| @@ -174,6 +176,7 @@ func New(cfg Config) (*Server, error) { | |||
| 174 | 176 | s := &Server{ | |
| 175 | 177 | cfg: cfg, instance: u.Host, assets: assets, md: newMarkdown(), | |
| 176 | 178 | pathCache: newPathCache(pathCacheBytes, pathBuilds), | |
| 179 | + | licenses: newPathCache(1<<20, homeWorkers), | |
| 177 | 180 | hl: newHLCache(hlCacheBytes), | |
| 178 | 181 | origin: originOf(u), | |
| 179 | 182 | } | |
| @@ -139,6 +139,8 @@ type Server struct { | |||||
| 139 | md *markdown | 139 | md *markdown | ||
| 140 | // pathCache holds the path lists the file finder fetches. | 140 | // pathCache holds the path lists the file finder fetches. | ||
| 141 | pathCache *pathCache | 141 | pathCache *pathCache | ||
| 142 | + | // licenses holds small display results, including missing licenses. | |||
| 143 | + | licenses *pathCache | |||
| 142 | // hl holds the colours of highlighted diffs. | 144 | // hl holds the colours of highlighted diffs. | ||
| 143 | hl *hlCache | 145 | hl *hlCache | ||
| 144 | mux *http.ServeMux | 146 | mux *http.ServeMux | ||
| @@ -174,6 +176,7 @@ func New(cfg Config) (*Server, error) { | |||||
| 174 | s := &Server{ | 176 | s := &Server{ | ||
| 175 | cfg: cfg, instance: u.Host, assets: assets, md: newMarkdown(), | 177 | cfg: cfg, instance: u.Host, assets: assets, md: newMarkdown(), | ||
| 176 | pathCache: newPathCache(pathCacheBytes, pathBuilds), | 178 | pathCache: newPathCache(pathCacheBytes, pathBuilds), | ||
| 179 | + | licenses: newPathCache(1<<20, homeWorkers), | |||
| 177 | hl: newHLCache(hlCacheBytes), | 180 | hl: newHLCache(hlCacheBytes), | ||
| 178 | origin: originOf(u), | 181 | origin: originOf(u), | ||
| 179 | } | 182 | } | ||
M internal/app/testdata/web/summary.html +2 -2
| @@ -99,7 +99,7 @@ | |||
| 99 | 99 | <p class="lede">A demo repository</p> | |
| 100 | 100 | </section> | |
| 101 | 101 | <main id="main" tabindex="-1"> | |
| 102 | - | <section class="card readme" aria-labelledby="readme-h"> | |
| 102 | + | <section class="card readme" aria-label="README.md"> | |
| 103 | 103 | <h2 class="card-h" id="readme-h"> | |
| 104 | 104 | <span class="ico i-file" aria-hidden="true"> | |
| 105 | 105 | </span> README.md</h2> | |
| @@ -164,7 +164,7 @@ | |||
| 164 | 164 | </h2> | |
| 165 | 165 | <table class="list commits" aria-label="Commits"> | |
| 166 | 166 | <tbody data-rows> | |
| 167 | - | <tr data-cursor> | |
| 167 | + | <tr> | |
| 168 | 168 | <td class="c-sha mute">SHA7</td> | |
| 169 | 169 | <td class="c-subject"> | |
| 170 | 170 | <a href="/demo/commit/SHA40">add main.go</a> | |
| @@ -99,7 +99,7 @@ | |||||
| 99 | <p class="lede">A demo repository</p> | 99 | <p class="lede">A demo repository</p> | ||
| 100 | </section> | 100 | </section> | ||
| 101 | <main id="main" tabindex="-1"> | 101 | <main id="main" tabindex="-1"> | ||
| 102 | - | <section class="card readme" aria-labelledby="readme-h"> | 102 | + | <section class="card readme" aria-label="README.md"> |
| 103 | <h2 class="card-h" id="readme-h"> | 103 | <h2 class="card-h" id="readme-h"> | ||
| 104 | <span class="ico i-file" aria-hidden="true"> | 104 | <span class="ico i-file" aria-hidden="true"> | ||
| 105 | </span> README.md</h2> | 105 | </span> README.md</h2> | ||
| @@ -164,7 +164,7 @@ | |||||
| 164 | </h2> | 164 | </h2> | ||
| 165 | <table class="list commits" aria-label="Commits"> | 165 | <table class="list commits" aria-label="Commits"> | ||
| 166 | <tbody data-rows> | 166 | <tbody data-rows> | ||
| 167 | - | <tr data-cursor> | 167 | + | <tr> |
| 168 | <td class="c-sha mute">SHA7</td> | 168 | <td class="c-sha mute">SHA7</td> | ||
| 169 | <td class="c-subject"> | 169 | <td class="c-subject"> | ||
| 170 | <a href="/demo/commit/SHA40">add main.go</a> | 170 | <a href="/demo/commit/SHA40">add main.go</a> | ||