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
| @@ -231,89 +231,179 @@ func HomeRowView(r HomeRow) templ.Component { | |||
| 231 | 231 | if templ_7745c5c3_Err != nil { | |
| 232 | 232 | return templ_7745c5c3_Err | |
| 233 | 233 | } | |
| 234 | + | if rel := r.Release; rel != nil { | |
| 235 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<a class=\"pill repo-release\" href=\"") | |
| 236 | + | if templ_7745c5c3_Err != nil { | |
| 237 | + | return templ_7745c5c3_Err | |
| 238 | + | } | |
| 239 | + | var templ_7745c5c3_Var11 templ.SafeURL | |
| 240 | + | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(rel.Href)) | |
| 241 | + | if templ_7745c5c3_Err != nil { | |
| 242 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 45, Col: 64} | |
| 243 | + | } | |
| 244 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | |
| 245 | + | if templ_7745c5c3_Err != nil { | |
| 246 | + | return templ_7745c5c3_Err | |
| 247 | + | } | |
| 248 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" title=\"") | |
| 249 | + | if templ_7745c5c3_Err != nil { | |
| 250 | + | return templ_7745c5c3_Err | |
| 251 | + | } | |
| 252 | + | var templ_7745c5c3_Var12 string | |
| 253 | + | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue("Latest release: " + rel.Tag) | |
| 254 | + | if templ_7745c5c3_Err != nil { | |
| 255 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 45, Col: 103} | |
| 256 | + | } | |
| 257 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12) | |
| 258 | + | if templ_7745c5c3_Err != nil { | |
| 259 | + | return templ_7745c5c3_Err | |
| 260 | + | } | |
| 261 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">") | |
| 262 | + | if templ_7745c5c3_Err != nil { | |
| 263 | + | return templ_7745c5c3_Err | |
| 264 | + | } | |
| 265 | + | var templ_7745c5c3_Var13 string | |
| 266 | + | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(rel.Tag) | |
| 267 | + | if templ_7745c5c3_Err != nil { | |
| 268 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 45, Col: 115} | |
| 269 | + | } | |
| 270 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | |
| 271 | + | if templ_7745c5c3_Err != nil { | |
| 272 | + | return templ_7745c5c3_Err | |
| 273 | + | } | |
| 274 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</a> ") | |
| 275 | + | if templ_7745c5c3_Err != nil { | |
| 276 | + | return templ_7745c5c3_Err | |
| 277 | + | } | |
| 278 | + | } | |
| 234 | 279 | if r.Private { | |
| 235 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"pill\">private</span> ") | |
| 280 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"pill\">private</span> ") | |
| 281 | + | if templ_7745c5c3_Err != nil { | |
| 282 | + | return templ_7745c5c3_Err | |
| 283 | + | } | |
| 284 | + | } | |
| 285 | + | if lic := r.License; lic != nil { | |
| 286 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<a class=\"pill repo-license\" href=\"") | |
| 287 | + | if templ_7745c5c3_Err != nil { | |
| 288 | + | return templ_7745c5c3_Err | |
| 289 | + | } | |
| 290 | + | var templ_7745c5c3_Var14 templ.SafeURL | |
| 291 | + | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(lic.Href)) | |
| 292 | + | if templ_7745c5c3_Err != nil { | |
| 293 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 51, Col: 64} | |
| 294 | + | } | |
| 295 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | |
| 296 | + | if templ_7745c5c3_Err != nil { | |
| 297 | + | return templ_7745c5c3_Err | |
| 298 | + | } | |
| 299 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" title=\"") | |
| 300 | + | if templ_7745c5c3_Err != nil { | |
| 301 | + | return templ_7745c5c3_Err | |
| 302 | + | } | |
| 303 | + | var templ_7745c5c3_Var15 string | |
| 304 | + | templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("License: " + lic.ID) | |
| 305 | + | if templ_7745c5c3_Err != nil { | |
| 306 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 51, Col: 95} | |
| 307 | + | } | |
| 308 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15) | |
| 309 | + | if templ_7745c5c3_Err != nil { | |
| 310 | + | return templ_7745c5c3_Err | |
| 311 | + | } | |
| 312 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">") | |
| 313 | + | if templ_7745c5c3_Err != nil { | |
| 314 | + | return templ_7745c5c3_Err | |
| 315 | + | } | |
| 316 | + | var templ_7745c5c3_Var16 string | |
| 317 | + | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(lic.ID) | |
| 318 | + | if templ_7745c5c3_Err != nil { | |
| 319 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 51, Col: 106} | |
| 320 | + | } | |
| 321 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | |
| 322 | + | if templ_7745c5c3_Err != nil { | |
| 323 | + | return templ_7745c5c3_Err | |
| 324 | + | } | |
| 325 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</a> ") | |
| 236 | 326 | if templ_7745c5c3_Err != nil { | |
| 237 | 327 | return templ_7745c5c3_Err | |
| 238 | 328 | } | |
| 239 | 329 | } | |
| 240 | 330 | if r.Fresh { | |
| 241 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"pill new\">new activity</span>") | |
| 331 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<span class=\"repo-activity\" title=\"new activity\"><span class=\"ico i-refresh-cw\" aria-hidden=\"true\"></span><span class=\"sr\">new activity</span></span>") | |
| 242 | 332 | if templ_7745c5c3_Err != nil { | |
| 243 | 333 | return templ_7745c5c3_Err | |
| 244 | 334 | } | |
| 245 | 335 | } | |
| 246 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</h2>") | |
| 336 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</h2>") | |
| 247 | 337 | if templ_7745c5c3_Err != nil { | |
| 248 | 338 | return templ_7745c5c3_Err | |
| 249 | 339 | } | |
| 250 | 340 | if r.Desc != "" { | |
| 251 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p class=\"repo-desc\">") | |
| 341 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<p class=\"repo-desc\">") | |
| 252 | 342 | if templ_7745c5c3_Err != nil { | |
| 253 | 343 | return templ_7745c5c3_Err | |
| 254 | 344 | } | |
| 255 | - | var templ_7745c5c3_Var11 string | |
| 256 | - | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.Desc) | |
| 345 | + | var templ_7745c5c3_Var17 string | |
| 346 | + | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(r.Desc) | |
| 257 | 347 | if templ_7745c5c3_Err != nil { | |
| 258 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 52, Col: 33} | |
| 348 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 58, Col: 33} | |
| 259 | 349 | } | |
| 260 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | |
| 350 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) | |
| 261 | 351 | if templ_7745c5c3_Err != nil { | |
| 262 | 352 | return templ_7745c5c3_Err | |
| 263 | 353 | } | |
| 264 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p>") | |
| 354 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</p>") | |
| 265 | 355 | if templ_7745c5c3_Err != nil { | |
| 266 | 356 | return templ_7745c5c3_Err | |
| 267 | 357 | } | |
| 268 | 358 | } else { | |
| 269 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p class=\"repo-desc mute\">no description</p>") | |
| 359 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<p class=\"repo-desc mute\">no description</p>") | |
| 270 | 360 | if templ_7745c5c3_Err != nil { | |
| 271 | 361 | return templ_7745c5c3_Err | |
| 272 | 362 | } | |
| 273 | 363 | } | |
| 274 | 364 | if len(r.Topics) > 0 { | |
| 275 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<ul class=\"repo-topics\" aria-label=\"") | |
| 365 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<ul class=\"repo-topics\" aria-label=\"") | |
| 276 | 366 | if templ_7745c5c3_Err != nil { | |
| 277 | 367 | return templ_7745c5c3_Err | |
| 278 | 368 | } | |
| 279 | - | var templ_7745c5c3_Var12 string | |
| 280 | - | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue("Topics of " + r.Name) | |
| 369 | + | var templ_7745c5c3_Var18 string | |
| 370 | + | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("Topics of " + r.Name) | |
| 281 | 371 | if templ_7745c5c3_Err != nil { | |
| 282 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 57, Col: 62} | |
| 372 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 63, Col: 62} | |
| 283 | 373 | } | |
| 284 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12) | |
| 374 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) | |
| 285 | 375 | if templ_7745c5c3_Err != nil { | |
| 286 | 376 | return templ_7745c5c3_Err | |
| 287 | 377 | } | |
| 288 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" data-fit>") | |
| 378 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" data-fit>") | |
| 289 | 379 | if templ_7745c5c3_Err != nil { | |
| 290 | 380 | return templ_7745c5c3_Err | |
| 291 | 381 | } | |
| 292 | 382 | for _, t := range r.Topics { | |
| 293 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<li>") | |
| 383 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<li>") | |
| 294 | 384 | if templ_7745c5c3_Err != nil { | |
| 295 | 385 | return templ_7745c5c3_Err | |
| 296 | 386 | } | |
| 297 | - | var templ_7745c5c3_Var13 string | |
| 298 | - | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(t) | |
| 387 | + | var templ_7745c5c3_Var19 string | |
| 388 | + | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(t) | |
| 299 | 389 | if templ_7745c5c3_Err != nil { | |
| 300 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 59, Col: 13} | |
| 390 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 65, Col: 13} | |
| 301 | 391 | } | |
| 302 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | |
| 392 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | |
| 303 | 393 | if templ_7745c5c3_Err != nil { | |
| 304 | 394 | return templ_7745c5c3_Err | |
| 305 | 395 | } | |
| 306 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</li>") | |
| 396 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</li>") | |
| 307 | 397 | if templ_7745c5c3_Err != nil { | |
| 308 | 398 | return templ_7745c5c3_Err | |
| 309 | 399 | } | |
| 310 | 400 | } | |
| 311 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</ul>") | |
| 401 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</ul>") | |
| 312 | 402 | if templ_7745c5c3_Err != nil { | |
| 313 | 403 | return templ_7745c5c3_Err | |
| 314 | 404 | } | |
| 315 | 405 | } else { | |
| 316 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"repo-topics\" aria-hidden=\"true\"></div>") | |
| 406 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"repo-topics\" aria-hidden=\"true\"></div>") | |
| 317 | 407 | if templ_7745c5c3_Err != nil { | |
| 318 | 408 | return templ_7745c5c3_Err | |
| 319 | 409 | } | |
| @@ -324,425 +414,425 @@ func HomeRowView(r HomeRow) templ.Component { | |||
| 324 | 414 | return templ_7745c5c3_Err | |
| 325 | 415 | } | |
| 326 | 416 | } else { | |
| 327 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div class=\"repo-pulse\" aria-hidden=\"true\"></div>") | |
| 417 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"repo-pulse\" aria-hidden=\"true\"></div>") | |
| 328 | 418 | if templ_7745c5c3_Err != nil { | |
| 329 | 419 | return templ_7745c5c3_Err | |
| 330 | 420 | } | |
| 331 | 421 | } | |
| 332 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><div class=\"repo-head\"><dl class=\"repo-stats\" aria-label=\"") | |
| 422 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</div><div class=\"repo-head\"><dl class=\"repo-stats\" aria-label=\"") | |
| 333 | 423 | if templ_7745c5c3_Err != nil { | |
| 334 | 424 | return templ_7745c5c3_Err | |
| 335 | 425 | } | |
| 336 | - | var templ_7745c5c3_Var14 string | |
| 337 | - | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue("Figures of " + r.Name) | |
| 426 | + | var templ_7745c5c3_Var20 string | |
| 427 | + | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue("Figures of " + r.Name) | |
| 338 | 428 | if templ_7745c5c3_Err != nil { | |
| 339 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 72, Col: 61} | |
| 429 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 78, Col: 61} | |
| 340 | 430 | } | |
| 341 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14) | |
| 431 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20) | |
| 342 | 432 | if templ_7745c5c3_Err != nil { | |
| 343 | 433 | return templ_7745c5c3_Err | |
| 344 | 434 | } | |
| 345 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">") | |
| 435 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\">") | |
| 346 | 436 | if templ_7745c5c3_Err != nil { | |
| 347 | 437 | return templ_7745c5c3_Err | |
| 348 | 438 | } | |
| 349 | 439 | if s := r.Stats; s != nil { | |
| 350 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div><dt>commits</dt><dd>") | |
| 440 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<div><dt>commits</dt><dd>") | |
| 351 | 441 | if templ_7745c5c3_Err != nil { | |
| 352 | 442 | return templ_7745c5c3_Err | |
| 353 | 443 | } | |
| 354 | - | var templ_7745c5c3_Var15 string | |
| 355 | - | templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(s.Commits) | |
| 444 | + | var templ_7745c5c3_Var21 string | |
| 445 | + | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(s.Commits) | |
| 356 | 446 | if templ_7745c5c3_Err != nil { | |
| 357 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 74, Col: 41} | |
| 447 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 80, Col: 41} | |
| 358 | 448 | } | |
| 359 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) | |
| 449 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) | |
| 360 | 450 | if templ_7745c5c3_Err != nil { | |
| 361 | 451 | return templ_7745c5c3_Err | |
| 362 | 452 | } | |
| 363 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</dd></div><div><dt>last 30 days</dt><dd>") | |
| 453 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</dd></div><div><dt>last 30 days</dt><dd>") | |
| 364 | 454 | if templ_7745c5c3_Err != nil { | |
| 365 | 455 | return templ_7745c5c3_Err | |
| 366 | 456 | } | |
| 367 | - | var templ_7745c5c3_Var16 string | |
| 368 | - | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(s.Recent) | |
| 457 | + | var templ_7745c5c3_Var22 string | |
| 458 | + | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(s.Recent) | |
| 369 | 459 | if templ_7745c5c3_Err != nil { | |
| 370 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 75, Col: 45} | |
| 460 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 81, Col: 45} | |
| 371 | 461 | } | |
| 372 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | |
| 462 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | |
| 373 | 463 | if templ_7745c5c3_Err != nil { | |
| 374 | 464 | return templ_7745c5c3_Err | |
| 375 | 465 | } | |
| 376 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</dd></div>") | |
| 466 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</dd></div>") | |
| 377 | 467 | if templ_7745c5c3_Err != nil { | |
| 378 | 468 | return templ_7745c5c3_Err | |
| 379 | 469 | } | |
| 380 | 470 | } | |
| 381 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div class=\"rs-active\"><dt>last activity</dt>") | |
| 471 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"rs-active\"><dt>last activity</dt>") | |
| 382 | 472 | if templ_7745c5c3_Err != nil { | |
| 383 | 473 | return templ_7745c5c3_Err | |
| 384 | 474 | } | |
| 385 | 475 | if r.ActiveAt != "" { | |
| 386 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<dd><time datetime=\"") | |
| 476 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<dd><time datetime=\"") | |
| 387 | 477 | if templ_7745c5c3_Err != nil { | |
| 388 | 478 | return templ_7745c5c3_Err | |
| 389 | 479 | } | |
| 390 | - | var templ_7745c5c3_Var17 string | |
| 391 | - | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ActiveAt) | |
| 480 | + | var templ_7745c5c3_Var23 string | |
| 481 | + | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ActiveAt) | |
| 392 | 482 | if templ_7745c5c3_Err != nil { | |
| 393 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 80, Col: 37} | |
| 483 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 86, Col: 37} | |
| 394 | 484 | } | |
| 395 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) | |
| 485 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) | |
| 396 | 486 | if templ_7745c5c3_Err != nil { | |
| 397 | 487 | return templ_7745c5c3_Err | |
| 398 | 488 | } | |
| 399 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" title=\"") | |
| 489 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" title=\"") | |
| 400 | 490 | if templ_7745c5c3_Err != nil { | |
| 401 | 491 | return templ_7745c5c3_Err | |
| 402 | 492 | } | |
| 403 | - | var templ_7745c5c3_Var18 string | |
| 404 | - | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ActiveAt) | |
| 493 | + | var templ_7745c5c3_Var24 string | |
| 494 | + | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ActiveAt) | |
| 405 | 495 | if templ_7745c5c3_Err != nil { | |
| 406 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 80, Col: 58} | |
| 496 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 86, Col: 58} | |
| 407 | 497 | } | |
| 408 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) | |
| 498 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) | |
| 409 | 499 | if templ_7745c5c3_Err != nil { | |
| 410 | 500 | return templ_7745c5c3_Err | |
| 411 | 501 | } | |
| 412 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">") | |
| 502 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">") | |
| 413 | 503 | if templ_7745c5c3_Err != nil { | |
| 414 | 504 | return templ_7745c5c3_Err | |
| 415 | 505 | } | |
| 416 | - | var templ_7745c5c3_Var19 string | |
| 417 | - | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(r.Active) | |
| 506 | + | var templ_7745c5c3_Var25 string | |
| 507 | + | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(r.Active) | |
| 418 | 508 | if templ_7745c5c3_Err != nil { | |
| 419 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 80, Col: 71} | |
| 509 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 86, Col: 71} | |
| 420 | 510 | } | |
| 421 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | |
| 511 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) | |
| 422 | 512 | if templ_7745c5c3_Err != nil { | |
| 423 | 513 | return templ_7745c5c3_Err | |
| 424 | 514 | } | |
| 425 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</time></dd>") | |
| 515 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</time></dd>") | |
| 426 | 516 | if templ_7745c5c3_Err != nil { | |
| 427 | 517 | return templ_7745c5c3_Err | |
| 428 | 518 | } | |
| 429 | 519 | } else { | |
| 430 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<dd class=\"mute\">never</dd>") | |
| 520 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<dd class=\"mute\">never</dd>") | |
| 431 | 521 | if templ_7745c5c3_Err != nil { | |
| 432 | 522 | return templ_7745c5c3_Err | |
| 433 | 523 | } | |
| 434 | 524 | } | |
| 435 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div>") | |
| 525 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div>") | |
| 436 | 526 | if templ_7745c5c3_Err != nil { | |
| 437 | 527 | return templ_7745c5c3_Err | |
| 438 | 528 | } | |
| 439 | 529 | if s := r.Stats; s != nil && s.Tag != "" { | |
| 440 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<div class=\"rs-tag\"><dt>last tag</dt>") | |
| 530 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div class=\"rs-tag\"><dt>last tag</dt>") | |
| 441 | 531 | if templ_7745c5c3_Err != nil { | |
| 442 | 532 | return templ_7745c5c3_Err | |
| 443 | 533 | } | |
| 444 | 534 | if s.TagHref != "" { | |
| 445 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<dd><a href=\"") | |
| 535 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<dd><a href=\"") | |
| 446 | 536 | if templ_7745c5c3_Err != nil { | |
| 447 | 537 | return templ_7745c5c3_Err | |
| 448 | 538 | } | |
| 449 | - | var templ_7745c5c3_Var20 templ.SafeURL | |
| 450 | - | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.TagHref)) | |
| 539 | + | var templ_7745c5c3_Var26 templ.SafeURL | |
| 540 | + | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.TagHref)) | |
| 451 | 541 | if templ_7745c5c3_Err != nil { | |
| 452 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 89, Col: 45} | |
| 542 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 95, Col: 45} | |
| 453 | 543 | } | |
| 454 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) | |
| 544 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | |
| 455 | 545 | if templ_7745c5c3_Err != nil { | |
| 456 | 546 | return templ_7745c5c3_Err | |
| 457 | 547 | } | |
| 458 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" title=\"") | |
| 548 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" title=\"") | |
| 459 | 549 | if templ_7745c5c3_Err != nil { | |
| 460 | 550 | return templ_7745c5c3_Err | |
| 461 | 551 | } | |
| 462 | - | var templ_7745c5c3_Var21 string | |
| 463 | - | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Tag + " · made " + s.TagWhen) | |
| 552 | + | var templ_7745c5c3_Var27 string | |
| 553 | + | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Tag + " · made " + s.TagWhen) | |
| 464 | 554 | if templ_7745c5c3_Err != nil { | |
| 465 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 89, Col: 87} | |
| 555 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 95, Col: 87} | |
| 466 | 556 | } | |
| 467 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21) | |
| 557 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27) | |
| 468 | 558 | if templ_7745c5c3_Err != nil { | |
| 469 | 559 | return templ_7745c5c3_Err | |
| 470 | 560 | } | |
| 471 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">") | |
| 561 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\">") | |
| 472 | 562 | if templ_7745c5c3_Err != nil { | |
| 473 | 563 | return templ_7745c5c3_Err | |
| 474 | 564 | } | |
| 475 | - | var templ_7745c5c3_Var22 string | |
| 476 | - | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(s.Tag) | |
| 565 | + | var templ_7745c5c3_Var28 string | |
| 566 | + | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(s.Tag) | |
| 477 | 567 | if templ_7745c5c3_Err != nil { | |
| 478 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 89, Col: 97} | |
| 568 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 95, Col: 97} | |
| 479 | 569 | } | |
| 480 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | |
| 570 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) | |
| 481 | 571 | if templ_7745c5c3_Err != nil { | |
| 482 | 572 | return templ_7745c5c3_Err | |
| 483 | 573 | } | |
| 484 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</a></dd>") | |
| 574 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</a></dd>") | |
| 485 | 575 | if templ_7745c5c3_Err != nil { | |
| 486 | 576 | return templ_7745c5c3_Err | |
| 487 | 577 | } | |
| 488 | 578 | } else { | |
| 489 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<dd title=\"") | |
| 579 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<dd title=\"") | |
| 490 | 580 | if templ_7745c5c3_Err != nil { | |
| 491 | 581 | return templ_7745c5c3_Err | |
| 492 | 582 | } | |
| 493 | - | var templ_7745c5c3_Var23 string | |
| 494 | - | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Tag + " · made " + s.TagWhen) | |
| 583 | + | var templ_7745c5c3_Var29 string | |
| 584 | + | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Tag + " · made " + s.TagWhen) | |
| 495 | 585 | if templ_7745c5c3_Err != nil { | |
| 496 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 91, Col: 50} | |
| 586 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 97, Col: 50} | |
| 497 | 587 | } | |
| 498 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) | |
| 588 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29) | |
| 499 | 589 | if templ_7745c5c3_Err != nil { | |
| 500 | 590 | return templ_7745c5c3_Err | |
| 501 | 591 | } | |
| 502 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">") | |
| 592 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\">") | |
| 503 | 593 | if templ_7745c5c3_Err != nil { | |
| 504 | 594 | return templ_7745c5c3_Err | |
| 505 | 595 | } | |
| 506 | - | var templ_7745c5c3_Var24 string | |
| 507 | - | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(s.Tag) | |
| 596 | + | var templ_7745c5c3_Var30 string | |
| 597 | + | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(s.Tag) | |
| 508 | 598 | if templ_7745c5c3_Err != nil { | |
| 509 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 91, Col: 60} | |
| 599 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 97, Col: 60} | |
| 510 | 600 | } | |
| 511 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) | |
| 601 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | |
| 512 | 602 | if templ_7745c5c3_Err != nil { | |
| 513 | 603 | return templ_7745c5c3_Err | |
| 514 | 604 | } | |
| 515 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</dd>") | |
| 605 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</dd>") | |
| 516 | 606 | if templ_7745c5c3_Err != nil { | |
| 517 | 607 | return templ_7745c5c3_Err | |
| 518 | 608 | } | |
| 519 | 609 | } | |
| 520 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</div>") | |
| 610 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div>") | |
| 521 | 611 | if templ_7745c5c3_Err != nil { | |
| 522 | 612 | return templ_7745c5c3_Err | |
| 523 | 613 | } | |
| 524 | 614 | } | |
| 525 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</dl>") | |
| 615 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</dl>") | |
| 526 | 616 | if templ_7745c5c3_Err != nil { | |
| 527 | 617 | return templ_7745c5c3_Err | |
| 528 | 618 | } | |
| 529 | 619 | if r.CI != nil { | |
| 530 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<a class=\"repo-ci\" href=\"") | |
| 620 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<a class=\"repo-ci\" href=\"") | |
| 531 | 621 | if templ_7745c5c3_Err != nil { | |
| 532 | 622 | return templ_7745c5c3_Err | |
| 533 | 623 | } | |
| 534 | - | var templ_7745c5c3_Var25 templ.SafeURL | |
| 535 | - | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CI.Href)) | |
| 624 | + | var templ_7745c5c3_Var31 templ.SafeURL | |
| 625 | + | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CI.Href)) | |
| 536 | 626 | if templ_7745c5c3_Err != nil { | |
| 537 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 97, Col: 54} | |
| 627 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 103, Col: 54} | |
| 538 | 628 | } | |
| 539 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) | |
| 629 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 540 | 630 | if templ_7745c5c3_Err != nil { | |
| 541 | 631 | return templ_7745c5c3_Err | |
| 542 | 632 | } | |
| 543 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" title=\"") | |
| 633 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" title=\"") | |
| 544 | 634 | if templ_7745c5c3_Err != nil { | |
| 545 | 635 | return templ_7745c5c3_Err | |
| 546 | 636 | } | |
| 547 | - | var templ_7745c5c3_Var26 string | |
| 548 | - | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CI.Title) | |
| 637 | + | var templ_7745c5c3_Var32 string | |
| 638 | + | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CI.Label + " · " + r.CI.Title) | |
| 549 | 639 | if templ_7745c5c3_Err != nil { | |
| 550 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 97, Col: 75} | |
| 640 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 103, Col: 97} | |
| 551 | 641 | } | |
| 552 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26) | |
| 642 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) | |
| 553 | 643 | if templ_7745c5c3_Err != nil { | |
| 554 | 644 | return templ_7745c5c3_Err | |
| 555 | 645 | } | |
| 556 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">") | |
| 646 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\">") | |
| 557 | 647 | if templ_7745c5c3_Err != nil { | |
| 558 | 648 | return templ_7745c5c3_Err | |
| 559 | 649 | } | |
| 560 | - | var templ_7745c5c3_Var27 = []any{"ck", "ck-" + r.CI.Kind} | |
| 561 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var27...) | |
| 650 | + | var templ_7745c5c3_Var33 = []any{"ck", "ck-" + r.CI.Kind} | |
| 651 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var33...) | |
| 562 | 652 | if templ_7745c5c3_Err != nil { | |
| 563 | 653 | return templ_7745c5c3_Err | |
| 564 | 654 | } | |
| 565 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<span class=\"") | |
| 655 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<span class=\"") | |
| 566 | 656 | if templ_7745c5c3_Err != nil { | |
| 567 | 657 | return templ_7745c5c3_Err | |
| 568 | 658 | } | |
| 569 | - | var templ_7745c5c3_Var28 string | |
| 570 | - | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var27).String()) | |
| 659 | + | var templ_7745c5c3_Var34 string | |
| 660 | + | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var33).String()) | |
| 571 | 661 | if templ_7745c5c3_Err != nil { | |
| 572 | 662 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 1, Col: 0} | |
| 573 | 663 | } | |
| 574 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) | |
| 664 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34) | |
| 575 | 665 | if templ_7745c5c3_Err != nil { | |
| 576 | 666 | return templ_7745c5c3_Err | |
| 577 | 667 | } | |
| 578 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" aria-hidden=\"true\"></span> <span class=\"sr\">CI: </span><span class=\"ci-label\">") | |
| 668 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" aria-hidden=\"true\"></span> <span class=\"sr\">") | |
| 579 | 669 | if templ_7745c5c3_Err != nil { | |
| 580 | 670 | return templ_7745c5c3_Err | |
| 581 | 671 | } | |
| 582 | - | var templ_7745c5c3_Var29 string | |
| 583 | - | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(r.CI.Label) | |
| 672 | + | var templ_7745c5c3_Var35 string | |
| 673 | + | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs("CI: " + r.CI.Label) | |
| 584 | 674 | if templ_7745c5c3_Err != nil { | |
| 585 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 99, Col: 68} | |
| 675 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 105, Col: 43} | |
| 586 | 676 | } | |
| 587 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | |
| 677 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 588 | 678 | if templ_7745c5c3_Err != nil { | |
| 589 | 679 | return templ_7745c5c3_Err | |
| 590 | 680 | } | |
| 591 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span></a>") | |
| 681 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</span></a>") | |
| 592 | 682 | if templ_7745c5c3_Err != nil { | |
| 593 | 683 | return templ_7745c5c3_Err | |
| 594 | 684 | } | |
| 595 | 685 | } | |
| 596 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div>") | |
| 686 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</div>") | |
| 597 | 687 | if templ_7745c5c3_Err != nil { | |
| 598 | 688 | return templ_7745c5c3_Err | |
| 599 | 689 | } | |
| 600 | 690 | if len(r.Commits) > 0 { | |
| 601 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<ul class=\"repo-commits\"") | |
| 691 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<ul class=\"repo-commits\"") | |
| 602 | 692 | if templ_7745c5c3_Err != nil { | |
| 603 | 693 | return templ_7745c5c3_Err | |
| 604 | 694 | } | |
| 605 | 695 | if len(r.Commits) == homeRecent { | |
| 606 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " data-full") | |
| 696 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, " data-full") | |
| 607 | 697 | if templ_7745c5c3_Err != nil { | |
| 608 | 698 | return templ_7745c5c3_Err | |
| 609 | 699 | } | |
| 610 | 700 | } | |
| 611 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " aria-label=\"") | |
| 701 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " aria-label=\"") | |
| 612 | 702 | if templ_7745c5c3_Err != nil { | |
| 613 | 703 | return templ_7745c5c3_Err | |
| 614 | 704 | } | |
| 615 | - | var templ_7745c5c3_Var30 string | |
| 616 | - | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue("Recent commits of " + r.Name) | |
| 705 | + | var templ_7745c5c3_Var36 string | |
| 706 | + | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue("Recent commits of " + r.Name) | |
| 617 | 707 | if templ_7745c5c3_Err != nil { | |
| 618 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 107, Col: 46} | |
| 708 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 113, Col: 46} | |
| 619 | 709 | } | |
| 620 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30) | |
| 710 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36) | |
| 621 | 711 | if templ_7745c5c3_Err != nil { | |
| 622 | 712 | return templ_7745c5c3_Err | |
| 623 | 713 | } | |
| 624 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\">") | |
| 714 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\">") | |
| 625 | 715 | if templ_7745c5c3_Err != nil { | |
| 626 | 716 | return templ_7745c5c3_Err | |
| 627 | 717 | } | |
| 628 | 718 | for _, c := range r.Commits { | |
| 629 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<li><a class=\"rc-sha\" href=\"") | |
| 719 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<li><a class=\"rc-sha\" href=\"") | |
| 630 | 720 | if templ_7745c5c3_Err != nil { | |
| 631 | 721 | return templ_7745c5c3_Err | |
| 632 | 722 | } | |
| 633 | - | var templ_7745c5c3_Var31 templ.SafeURL | |
| 634 | - | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 723 | + | var templ_7745c5c3_Var37 templ.SafeURL | |
| 724 | + | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 635 | 725 | if templ_7745c5c3_Err != nil { | |
| 636 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 111, Col: 53} | |
| 726 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 117, Col: 53} | |
| 637 | 727 | } | |
| 638 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 728 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) | |
| 639 | 729 | if templ_7745c5c3_Err != nil { | |
| 640 | 730 | return templ_7745c5c3_Err | |
| 641 | 731 | } | |
| 642 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\">") | |
| 732 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\">") | |
| 643 | 733 | if templ_7745c5c3_Err != nil { | |
| 644 | 734 | return templ_7745c5c3_Err | |
| 645 | 735 | } | |
| 646 | - | var templ_7745c5c3_Var32 string | |
| 647 | - | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 736 | + | var templ_7745c5c3_Var38 string | |
| 737 | + | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 648 | 738 | if templ_7745c5c3_Err != nil { | |
| 649 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 111, Col: 65} | |
| 739 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 117, Col: 65} | |
| 650 | 740 | } | |
| 651 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | |
| 741 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) | |
| 652 | 742 | if templ_7745c5c3_Err != nil { | |
| 653 | 743 | return templ_7745c5c3_Err | |
| 654 | 744 | } | |
| 655 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</a> <span class=\"rc-subject\" title=\"") | |
| 745 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</a> <span class=\"rc-subject\" title=\"") | |
| 656 | 746 | if templ_7745c5c3_Err != nil { | |
| 657 | 747 | return templ_7745c5c3_Err | |
| 658 | 748 | } | |
| 659 | - | var templ_7745c5c3_Var33 string | |
| 660 | - | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Subject) | |
| 749 | + | var templ_7745c5c3_Var39 string | |
| 750 | + | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Subject) | |
| 661 | 751 | if templ_7745c5c3_Err != nil { | |
| 662 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 112, Col: 48} | |
| 752 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 118, Col: 48} | |
| 663 | 753 | } | |
| 664 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) | |
| 754 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39) | |
| 665 | 755 | if templ_7745c5c3_Err != nil { | |
| 666 | 756 | return templ_7745c5c3_Err | |
| 667 | 757 | } | |
| 668 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">") | |
| 758 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\">") | |
| 669 | 759 | if templ_7745c5c3_Err != nil { | |
| 670 | 760 | return templ_7745c5c3_Err | |
| 671 | 761 | } | |
| 672 | - | var templ_7745c5c3_Var34 string | |
| 673 | - | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 762 | + | var templ_7745c5c3_Var40 string | |
| 763 | + | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 674 | 764 | if templ_7745c5c3_Err != nil { | |
| 675 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 112, Col: 62} | |
| 765 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 118, Col: 62} | |
| 676 | 766 | } | |
| 677 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | |
| 767 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | |
| 678 | 768 | if templ_7745c5c3_Err != nil { | |
| 679 | 769 | return templ_7745c5c3_Err | |
| 680 | 770 | } | |
| 681 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</span> ") | |
| 771 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</span> ") | |
| 682 | 772 | if templ_7745c5c3_Err != nil { | |
| 683 | 773 | return templ_7745c5c3_Err | |
| 684 | 774 | } | |
| 685 | 775 | if c.WhenAt != "" { | |
| 686 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<time class=\"dim\" datetime=\"") | |
| 776 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<time class=\"dim\" datetime=\"") | |
| 687 | 777 | if templ_7745c5c3_Err != nil { | |
| 688 | 778 | return templ_7745c5c3_Err | |
| 689 | 779 | } | |
| 690 | - | var templ_7745c5c3_Var35 string | |
| 691 | - | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 780 | + | var templ_7745c5c3_Var41 string | |
| 781 | + | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 692 | 782 | if templ_7745c5c3_Err != nil { | |
| 693 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 114, Col: 44} | |
| 783 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 120, Col: 44} | |
| 694 | 784 | } | |
| 695 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35) | |
| 785 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var41) | |
| 696 | 786 | if templ_7745c5c3_Err != nil { | |
| 697 | 787 | return templ_7745c5c3_Err | |
| 698 | 788 | } | |
| 699 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" title=\"") | |
| 789 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\" title=\"") | |
| 700 | 790 | if templ_7745c5c3_Err != nil { | |
| 701 | 791 | return templ_7745c5c3_Err | |
| 702 | 792 | } | |
| 703 | - | var templ_7745c5c3_Var36 string | |
| 704 | - | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 793 | + | var templ_7745c5c3_Var42 string | |
| 794 | + | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 705 | 795 | if templ_7745c5c3_Err != nil { | |
| 706 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 114, Col: 63} | |
| 796 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 120, Col: 63} | |
| 707 | 797 | } | |
| 708 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36) | |
| 798 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42) | |
| 709 | 799 | if templ_7745c5c3_Err != nil { | |
| 710 | 800 | return templ_7745c5c3_Err | |
| 711 | 801 | } | |
| 712 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\">") | |
| 802 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "\">") | |
| 713 | 803 | if templ_7745c5c3_Err != nil { | |
| 714 | 804 | return templ_7745c5c3_Err | |
| 715 | 805 | } | |
| 716 | - | var templ_7745c5c3_Var37 string | |
| 717 | - | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 806 | + | var templ_7745c5c3_Var43 string | |
| 807 | + | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 718 | 808 | if templ_7745c5c3_Err != nil { | |
| 719 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 114, Col: 74} | |
| 809 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 120, Col: 74} | |
| 720 | 810 | } | |
| 721 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) | |
| 811 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | |
| 722 | 812 | if templ_7745c5c3_Err != nil { | |
| 723 | 813 | return templ_7745c5c3_Err | |
| 724 | 814 | } | |
| 725 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</time>") | |
| 815 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</time>") | |
| 726 | 816 | if templ_7745c5c3_Err != nil { | |
| 727 | 817 | return templ_7745c5c3_Err | |
| 728 | 818 | } | |
| 729 | 819 | } | |
| 730 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</li>") | |
| 820 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</li>") | |
| 731 | 821 | if templ_7745c5c3_Err != nil { | |
| 732 | 822 | return templ_7745c5c3_Err | |
| 733 | 823 | } | |
| 734 | 824 | } | |
| 735 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</ul>") | |
| 825 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</ul>") | |
| 736 | 826 | if templ_7745c5c3_Err != nil { | |
| 737 | 827 | return templ_7745c5c3_Err | |
| 738 | 828 | } | |
| 739 | 829 | } else if r.NoCommits { | |
| 740 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<p class=\"repo-commits mute\">no commits yet</p>") | |
| 830 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<p class=\"repo-commits mute\">no commits yet</p>") | |
| 741 | 831 | if templ_7745c5c3_Err != nil { | |
| 742 | 832 | return templ_7745c5c3_Err | |
| 743 | 833 | } | |
| 744 | 834 | } | |
| 745 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</li>") | |
| 835 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</li>") | |
| 746 | 836 | if templ_7745c5c3_Err != nil { | |
| 747 | 837 | return templ_7745c5c3_Err | |
| 748 | 838 | } | |
| @@ -770,113 +860,113 @@ func pulse(h *Heartbeat) templ.Component { | |||
| 770 | 860 | }() | |
| 771 | 861 | } | |
| 772 | 862 | ctx = templ.InitializeContext(ctx) | |
| 773 | - | templ_7745c5c3_Var38 := templ.GetChildren(ctx) | |
| 774 | - | if templ_7745c5c3_Var38 == nil { | |
| 775 | - | templ_7745c5c3_Var38 = templ.NopComponent | |
| 863 | + | templ_7745c5c3_Var44 := templ.GetChildren(ctx) | |
| 864 | + | if templ_7745c5c3_Var44 == nil { | |
| 865 | + | templ_7745c5c3_Var44 = templ.NopComponent | |
| 776 | 866 | } | |
| 777 | 867 | ctx = templ.ClearChildren(ctx) | |
| 778 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<figure class=\"repo-pulse\"") | |
| 868 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<figure class=\"repo-pulse\"") | |
| 779 | 869 | if templ_7745c5c3_Err != nil { | |
| 780 | 870 | return templ_7745c5c3_Err | |
| 781 | 871 | } | |
| 782 | 872 | if h.Live { | |
| 783 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, " data-live") | |
| 873 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, " data-live") | |
| 784 | 874 | if templ_7745c5c3_Err != nil { | |
| 785 | 875 | return templ_7745c5c3_Err | |
| 786 | 876 | } | |
| 787 | 877 | } | |
| 788 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "><svg viewBox=\"0 0 600 64\" role=\"img\" aria-label=\"") | |
| 878 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "><svg viewBox=\"0 0 600 64\" role=\"img\" aria-label=\"") | |
| 789 | 879 | if templ_7745c5c3_Err != nil { | |
| 790 | 880 | return templ_7745c5c3_Err | |
| 791 | 881 | } | |
| 792 | - | var templ_7745c5c3_Var39 string | |
| 793 | - | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Label) | |
| 882 | + | var templ_7745c5c3_Var45 string | |
| 883 | + | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Label) | |
| 794 | 884 | if templ_7745c5c3_Err != nil { | |
| 795 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 131, Col: 59} | |
| 885 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 137, Col: 59} | |
| 796 | 886 | } | |
| 797 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39) | |
| 887 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var45) | |
| 798 | 888 | if templ_7745c5c3_Err != nil { | |
| 799 | 889 | return templ_7745c5c3_Err | |
| 800 | 890 | } | |
| 801 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" focusable=\"false\"><defs><linearGradient id=\"") | |
| 891 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" focusable=\"false\"><defs><linearGradient id=\"") | |
| 802 | 892 | if templ_7745c5c3_Err != nil { | |
| 803 | 893 | return templ_7745c5c3_Err | |
| 804 | 894 | } | |
| 805 | - | var templ_7745c5c3_Var40 string | |
| 806 | - | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.GradID) | |
| 895 | + | var templ_7745c5c3_Var46 string | |
| 896 | + | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.GradID) | |
| 807 | 897 | if templ_7745c5c3_Err != nil { | |
| 808 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 133, Col: 33} | |
| 898 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 139, Col: 33} | |
| 809 | 899 | } | |
| 810 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40) | |
| 900 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var46) | |
| 811 | 901 | if templ_7745c5c3_Err != nil { | |
| 812 | 902 | return templ_7745c5c3_Err | |
| 813 | 903 | } | |
| 814 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop class=\"pulse-stop-top\" offset=\"0\"></stop> <stop class=\"pulse-stop-bottom\" offset=\"1\"></stop></linearGradient></defs> <path class=\"pulse-grid\" d=\"M0 21.5 H600 M0 42.5 H600 M0 63.5 H600\" vector-effect=\"non-scaling-stroke\"></path> <path class=\"pulse-area\" d=\"") | |
| 904 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop class=\"pulse-stop-top\" offset=\"0\"></stop> <stop class=\"pulse-stop-bottom\" offset=\"1\"></stop></linearGradient></defs> <path class=\"pulse-grid\" d=\"M0 21.5 H600 M0 42.5 H600 M0 63.5 H600\" vector-effect=\"non-scaling-stroke\"></path> <path class=\"pulse-area\" d=\"") | |
| 815 | 905 | if templ_7745c5c3_Err != nil { | |
| 816 | 906 | return templ_7745c5c3_Err | |
| 817 | 907 | } | |
| 818 | - | var templ_7745c5c3_Var41 string | |
| 819 | - | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Area) | |
| 908 | + | var templ_7745c5c3_Var47 string | |
| 909 | + | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Area) | |
| 820 | 910 | if templ_7745c5c3_Err != nil { | |
| 821 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 139, Col: 38} | |
| 911 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 145, Col: 38} | |
| 822 | 912 | } | |
| 823 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var41) | |
| 913 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47) | |
| 824 | 914 | if templ_7745c5c3_Err != nil { | |
| 825 | 915 | return templ_7745c5c3_Err | |
| 826 | 916 | } | |
| 827 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "\" fill=\"") | |
| 917 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\" fill=\"") | |
| 828 | 918 | if templ_7745c5c3_Err != nil { | |
| 829 | 919 | return templ_7745c5c3_Err | |
| 830 | 920 | } | |
| 831 | - | var templ_7745c5c3_Var42 string | |
| 832 | - | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.ResolveAttributeValue("url(#" + h.GradID + ")") | |
| 921 | + | var templ_7745c5c3_Var48 string | |
| 922 | + | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue("url(#" + h.GradID + ")") | |
| 833 | 923 | if templ_7745c5c3_Err != nil { | |
| 834 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 139, Col: 72} | |
| 924 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 145, Col: 72} | |
| 835 | 925 | } | |
| 836 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42) | |
| 926 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48) | |
| 837 | 927 | if templ_7745c5c3_Err != nil { | |
| 838 | 928 | return templ_7745c5c3_Err | |
| 839 | 929 | } | |
| 840 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\"></path> <path class=\"pulse-line\" d=\"") | |
| 930 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "\"></path> <path class=\"pulse-line\" d=\"") | |
| 841 | 931 | if templ_7745c5c3_Err != nil { | |
| 842 | 932 | return templ_7745c5c3_Err | |
| 843 | 933 | } | |
| 844 | - | var templ_7745c5c3_Var43 string | |
| 845 | - | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Line) | |
| 934 | + | var templ_7745c5c3_Var49 string | |
| 935 | + | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Line) | |
| 846 | 936 | if templ_7745c5c3_Err != nil { | |
| 847 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 140, Col: 38} | |
| 937 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 146, Col: 38} | |
| 848 | 938 | } | |
| 849 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var43) | |
| 939 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49) | |
| 850 | 940 | if templ_7745c5c3_Err != nil { | |
| 851 | 941 | return templ_7745c5c3_Err | |
| 852 | 942 | } | |
| 853 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\" vector-effect=\"non-scaling-stroke\"></path> <circle class=\"pulse-dot\" cx=\"") | |
| 943 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "\" vector-effect=\"non-scaling-stroke\"></path> <circle class=\"pulse-dot\" cx=\"") | |
| 854 | 944 | if templ_7745c5c3_Err != nil { | |
| 855 | 945 | return templ_7745c5c3_Err | |
| 856 | 946 | } | |
| 857 | - | var templ_7745c5c3_Var44 string | |
| 858 | - | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.X) | |
| 947 | + | var templ_7745c5c3_Var50 string | |
| 948 | + | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.X) | |
| 859 | 949 | if templ_7745c5c3_Err != nil { | |
| 860 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 141, Col: 37} | |
| 950 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 147, Col: 37} | |
| 861 | 951 | } | |
| 862 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var44) | |
| 952 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50) | |
| 863 | 953 | if templ_7745c5c3_Err != nil { | |
| 864 | 954 | return templ_7745c5c3_Err | |
| 865 | 955 | } | |
| 866 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\" cy=\"") | |
| 956 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "\" cy=\"") | |
| 867 | 957 | if templ_7745c5c3_Err != nil { | |
| 868 | 958 | return templ_7745c5c3_Err | |
| 869 | 959 | } | |
| 870 | - | var templ_7745c5c3_Var45 string | |
| 871 | - | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Y) | |
| 960 | + | var templ_7745c5c3_Var51 string | |
| 961 | + | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Y) | |
| 872 | 962 | if templ_7745c5c3_Err != nil { | |
| 873 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 141, Col: 48} | |
| 963 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 147, Col: 48} | |
| 874 | 964 | } | |
| 875 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var45) | |
| 965 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var51) | |
| 876 | 966 | if templ_7745c5c3_Err != nil { | |
| 877 | 967 | return templ_7745c5c3_Err | |
| 878 | 968 | } | |
| 879 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "\" r=\"3.5\"></circle></svg></figure>") | |
| 969 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\" r=\"3.5\"></circle></svg></figure>") | |
| 880 | 970 | if templ_7745c5c3_Err != nil { | |
| 881 | 971 | return templ_7745c5c3_Err | |
| 882 | 972 | } | |
| @@ -901,66 +991,66 @@ func Outline(headings []Heading) templ.Component { | |||
| 901 | 991 | }() | |
| 902 | 992 | } | |
| 903 | 993 | ctx = templ.InitializeContext(ctx) | |
| 904 | - | templ_7745c5c3_Var46 := templ.GetChildren(ctx) | |
| 905 | - | if templ_7745c5c3_Var46 == nil { | |
| 906 | - | templ_7745c5c3_Var46 = templ.NopComponent | |
| 994 | + | templ_7745c5c3_Var52 := templ.GetChildren(ctx) | |
| 995 | + | if templ_7745c5c3_Var52 == nil { | |
| 996 | + | templ_7745c5c3_Var52 = templ.NopComponent | |
| 907 | 997 | } | |
| 908 | 998 | ctx = templ.ClearChildren(ctx) | |
| 909 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<nav class=\"outline\" aria-label=\"Outline\">") | |
| 999 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<nav class=\"outline\" aria-label=\"Outline\">") | |
| 910 | 1000 | if templ_7745c5c3_Err != nil { | |
| 911 | 1001 | return templ_7745c5c3_Err | |
| 912 | 1002 | } | |
| 913 | 1003 | for _, h := range headings { | |
| 914 | - | var templ_7745c5c3_Var47 = []any{"l" + itoa(h.Level)} | |
| 915 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var47...) | |
| 1004 | + | var templ_7745c5c3_Var53 = []any{"l" + itoa(h.Level)} | |
| 1005 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var53...) | |
| 916 | 1006 | if templ_7745c5c3_Err != nil { | |
| 917 | 1007 | return templ_7745c5c3_Err | |
| 918 | 1008 | } | |
| 919 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<a class=\"") | |
| 1009 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<a class=\"") | |
| 920 | 1010 | if templ_7745c5c3_Err != nil { | |
| 921 | 1011 | return templ_7745c5c3_Err | |
| 922 | 1012 | } | |
| 923 | - | var templ_7745c5c3_Var48 string | |
| 924 | - | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var47).String()) | |
| 1013 | + | var templ_7745c5c3_Var54 string | |
| 1014 | + | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var53).String()) | |
| 925 | 1015 | if templ_7745c5c3_Err != nil { | |
| 926 | 1016 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 1, Col: 0} | |
| 927 | 1017 | } | |
| 928 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48) | |
| 1018 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var54) | |
| 929 | 1019 | if templ_7745c5c3_Err != nil { | |
| 930 | 1020 | return templ_7745c5c3_Err | |
| 931 | 1021 | } | |
| 932 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\" href=\"") | |
| 1022 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\" href=\"") | |
| 933 | 1023 | if templ_7745c5c3_Err != nil { | |
| 934 | 1024 | return templ_7745c5c3_Err | |
| 935 | 1025 | } | |
| 936 | - | var templ_7745c5c3_Var49 templ.SafeURL | |
| 937 | - | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("#" + h.ID)) | |
| 1026 | + | var templ_7745c5c3_Var55 templ.SafeURL | |
| 1027 | + | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("#" + h.ID)) | |
| 938 | 1028 | if templ_7745c5c3_Err != nil { | |
| 939 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 150, Col: 68} | |
| 1029 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 156, Col: 68} | |
| 940 | 1030 | } | |
| 941 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) | |
| 1031 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 942 | 1032 | if templ_7745c5c3_Err != nil { | |
| 943 | 1033 | return templ_7745c5c3_Err | |
| 944 | 1034 | } | |
| 945 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\">") | |
| 1035 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\">") | |
| 946 | 1036 | if templ_7745c5c3_Err != nil { | |
| 947 | 1037 | return templ_7745c5c3_Err | |
| 948 | 1038 | } | |
| 949 | - | var templ_7745c5c3_Var50 string | |
| 950 | - | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(h.Text) | |
| 1039 | + | var templ_7745c5c3_Var56 string | |
| 1040 | + | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(h.Text) | |
| 951 | 1041 | if templ_7745c5c3_Err != nil { | |
| 952 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 150, Col: 79} | |
| 1042 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 156, Col: 79} | |
| 953 | 1043 | } | |
| 954 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) | |
| 1044 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) | |
| 955 | 1045 | if templ_7745c5c3_Err != nil { | |
| 956 | 1046 | return templ_7745c5c3_Err | |
| 957 | 1047 | } | |
| 958 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</a>") | |
| 1048 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</a>") | |
| 959 | 1049 | if templ_7745c5c3_Err != nil { | |
| 960 | 1050 | return templ_7745c5c3_Err | |
| 961 | 1051 | } | |
| 962 | 1052 | } | |
| 963 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</nav>") | |
| 1053 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</nav>") | |
| 964 | 1054 | if templ_7745c5c3_Err != nil { | |
| 965 | 1055 | return templ_7745c5c3_Err | |
| 966 | 1056 | } | |
| @@ -969,7 +1059,7 @@ func Outline(headings []Heading) templ.Component { | |||
| 969 | 1059 | } | |
| 970 | 1060 | ||
| 971 | 1061 | // ReadmeCard is a README in a card whose header names the file. | |
| 972 | - | func ReadmeCard(r Readme) templ.Component { | |
| 1062 | + | func ReadmeCard(r Readme, docs ...DocTab) templ.Component { | |
| 973 | 1063 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 974 | 1064 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 975 | 1065 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| @@ -985,33 +1075,103 @@ func ReadmeCard(r Readme) templ.Component { | |||
| 985 | 1075 | }() | |
| 986 | 1076 | } | |
| 987 | 1077 | ctx = templ.InitializeContext(ctx) | |
| 988 | - | templ_7745c5c3_Var51 := templ.GetChildren(ctx) | |
| 989 | - | if templ_7745c5c3_Var51 == nil { | |
| 990 | - | templ_7745c5c3_Var51 = templ.NopComponent | |
| 1078 | + | templ_7745c5c3_Var57 := templ.GetChildren(ctx) | |
| 1079 | + | if templ_7745c5c3_Var57 == nil { | |
| 1080 | + | templ_7745c5c3_Var57 = templ.NopComponent | |
| 991 | 1081 | } | |
| 992 | 1082 | ctx = templ.ClearChildren(ctx) | |
| 993 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<section class=\"card readme\" aria-labelledby=\"readme-h\"><h2 class=\"card-h\" id=\"readme-h\"><span class=\"ico i-file\" aria-hidden=\"true\"></span> ") | |
| 1083 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "<section class=\"card readme\" aria-label=\"") | |
| 994 | 1084 | if templ_7745c5c3_Err != nil { | |
| 995 | 1085 | return templ_7745c5c3_Err | |
| 996 | 1086 | } | |
| 997 | - | var templ_7745c5c3_Var52 string | |
| 998 | - | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 1087 | + | var templ_7745c5c3_Var58 string | |
| 1088 | + | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name) | |
| 999 | 1089 | if templ_7745c5c3_Err != nil { | |
| 1000 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 160, Col: 11} | |
| 1090 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 163, Col: 49} | |
| 1001 | 1091 | } | |
| 1002 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) | |
| 1092 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var58) | |
| 1003 | 1093 | if templ_7745c5c3_Err != nil { | |
| 1004 | 1094 | return templ_7745c5c3_Err | |
| 1005 | 1095 | } | |
| 1006 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</h2>") | |
| 1096 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "\">") | |
| 1007 | 1097 | if templ_7745c5c3_Err != nil { | |
| 1008 | 1098 | return templ_7745c5c3_Err | |
| 1009 | 1099 | } | |
| 1100 | + | if len(docs) > 1 { | |
| 1101 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "<nav class=\"card-h doc-tabs\" aria-label=\"Repository documents\">") | |
| 1102 | + | if templ_7745c5c3_Err != nil { | |
| 1103 | + | return templ_7745c5c3_Err | |
| 1104 | + | } | |
| 1105 | + | for _, doc := range docs { | |
| 1106 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<a href=\"") | |
| 1107 | + | if templ_7745c5c3_Err != nil { | |
| 1108 | + | return templ_7745c5c3_Err | |
| 1109 | + | } | |
| 1110 | + | var templ_7745c5c3_Var59 templ.SafeURL | |
| 1111 | + | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(doc.Href)) | |
| 1112 | + | if templ_7745c5c3_Err != nil { | |
| 1113 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 167, Col: 38} | |
| 1114 | + | } | |
| 1115 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) | |
| 1116 | + | if templ_7745c5c3_Err != nil { | |
| 1117 | + | return templ_7745c5c3_Err | |
| 1118 | + | } | |
| 1119 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "\"") | |
| 1120 | + | if templ_7745c5c3_Err != nil { | |
| 1121 | + | return templ_7745c5c3_Err | |
| 1122 | + | } | |
| 1123 | + | if doc.Current { | |
| 1124 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, " aria-current=\"page\"") | |
| 1125 | + | if templ_7745c5c3_Err != nil { | |
| 1126 | + | return templ_7745c5c3_Err | |
| 1127 | + | } | |
| 1128 | + | } | |
| 1129 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, ">") | |
| 1130 | + | if templ_7745c5c3_Err != nil { | |
| 1131 | + | return templ_7745c5c3_Err | |
| 1132 | + | } | |
| 1133 | + | var templ_7745c5c3_Var60 string | |
| 1134 | + | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Name) | |
| 1135 | + | if templ_7745c5c3_Err != nil { | |
| 1136 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 171, Col: 16} | |
| 1137 | + | } | |
| 1138 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) | |
| 1139 | + | if templ_7745c5c3_Err != nil { | |
| 1140 | + | return templ_7745c5c3_Err | |
| 1141 | + | } | |
| 1142 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</a>") | |
| 1143 | + | if templ_7745c5c3_Err != nil { | |
| 1144 | + | return templ_7745c5c3_Err | |
| 1145 | + | } | |
| 1146 | + | } | |
| 1147 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</nav>") | |
| 1148 | + | if templ_7745c5c3_Err != nil { | |
| 1149 | + | return templ_7745c5c3_Err | |
| 1150 | + | } | |
| 1151 | + | } else { | |
| 1152 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<h2 class=\"card-h\" id=\"readme-h\"><span class=\"ico i-file\" aria-hidden=\"true\"></span> ") | |
| 1153 | + | if templ_7745c5c3_Err != nil { | |
| 1154 | + | return templ_7745c5c3_Err | |
| 1155 | + | } | |
| 1156 | + | var templ_7745c5c3_Var61 string | |
| 1157 | + | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |
| 1158 | + | if templ_7745c5c3_Err != nil { | |
| 1159 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 177, Col: 12} | |
| 1160 | + | } | |
| 1161 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | |
| 1162 | + | if templ_7745c5c3_Err != nil { | |
| 1163 | + | return templ_7745c5c3_Err | |
| 1164 | + | } | |
| 1165 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</h2>") | |
| 1166 | + | if templ_7745c5c3_Err != nil { | |
| 1167 | + | return templ_7745c5c3_Err | |
| 1168 | + | } | |
| 1169 | + | } | |
| 1010 | 1170 | templ_7745c5c3_Err = ReadmeView(r).Render(ctx, templ_7745c5c3_Buffer) | |
| 1011 | 1171 | if templ_7745c5c3_Err != nil { | |
| 1012 | 1172 | return templ_7745c5c3_Err | |
| 1013 | 1173 | } | |
| 1014 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "</section>") | |
| 1174 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</section>") | |
| 1015 | 1175 | if templ_7745c5c3_Err != nil { | |
| 1016 | 1176 | return templ_7745c5c3_Err | |
| 1017 | 1177 | } | |
| @@ -1037,43 +1197,43 @@ func ReadmeView(r Readme) templ.Component { | |||
| 1037 | 1197 | }() | |
| 1038 | 1198 | } | |
| 1039 | 1199 | ctx = templ.InitializeContext(ctx) | |
| 1040 | - | templ_7745c5c3_Var53 := templ.GetChildren(ctx) | |
| 1041 | - | if templ_7745c5c3_Var53 == nil { | |
| 1042 | - | templ_7745c5c3_Var53 = templ.NopComponent | |
| 1200 | + | templ_7745c5c3_Var62 := templ.GetChildren(ctx) | |
| 1201 | + | if templ_7745c5c3_Var62 == nil { | |
| 1202 | + | templ_7745c5c3_Var62 = templ.NopComponent | |
| 1043 | 1203 | } | |
| 1044 | 1204 | ctx = templ.ClearChildren(ctx) | |
| 1045 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<article class=\"prose\" aria-label=\"") | |
| 1205 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<article class=\"prose\" aria-label=\"") | |
| 1046 | 1206 | if templ_7745c5c3_Err != nil { | |
| 1047 | 1207 | return templ_7745c5c3_Err | |
| 1048 | 1208 | } | |
| 1049 | - | var templ_7745c5c3_Var54 string | |
| 1050 | - | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name) | |
| 1209 | + | var templ_7745c5c3_Var63 string | |
| 1210 | + | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name) | |
| 1051 | 1211 | if templ_7745c5c3_Err != nil { | |
| 1052 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 169, Col: 43} | |
| 1212 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 187, Col: 43} | |
| 1053 | 1213 | } | |
| 1054 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var54) | |
| 1214 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63) | |
| 1055 | 1215 | if templ_7745c5c3_Err != nil { | |
| 1056 | 1216 | return templ_7745c5c3_Err | |
| 1057 | 1217 | } | |
| 1058 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\" hx-ignore>") | |
| 1218 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\" hx-ignore>") | |
| 1059 | 1219 | if templ_7745c5c3_Err != nil { | |
| 1060 | 1220 | return templ_7745c5c3_Err | |
| 1061 | 1221 | } | |
| 1062 | 1222 | if r.Note != "" { | |
| 1063 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "<p class=\"dim\">") | |
| 1223 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<p class=\"dim\">") | |
| 1064 | 1224 | if templ_7745c5c3_Err != nil { | |
| 1065 | 1225 | return templ_7745c5c3_Err | |
| 1066 | 1226 | } | |
| 1067 | - | var templ_7745c5c3_Var55 string | |
| 1068 | - | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(r.Note) | |
| 1227 | + | var templ_7745c5c3_Var64 string | |
| 1228 | + | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(r.Note) | |
| 1069 | 1229 | if templ_7745c5c3_Err != nil { | |
| 1070 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 171, Col: 26} | |
| 1230 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 189, Col: 26} | |
| 1071 | 1231 | } | |
| 1072 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 1232 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64)) | |
| 1073 | 1233 | if templ_7745c5c3_Err != nil { | |
| 1074 | 1234 | return templ_7745c5c3_Err | |
| 1075 | 1235 | } | |
| 1076 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</p>") | |
| 1236 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</p>") | |
| 1077 | 1237 | if templ_7745c5c3_Err != nil { | |
| 1078 | 1238 | return templ_7745c5c3_Err | |
| 1079 | 1239 | } | |
| @@ -1083,7 +1243,7 @@ func ReadmeView(r Readme) templ.Component { | |||
| 1083 | 1243 | return templ_7745c5c3_Err | |
| 1084 | 1244 | } | |
| 1085 | 1245 | } | |
| 1086 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</article>") | |
| 1246 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</article>") | |
| 1087 | 1247 | if templ_7745c5c3_Err != nil { | |
| 1088 | 1248 | return templ_7745c5c3_Err | |
| 1089 | 1249 | } | |
| @@ -1109,90 +1269,90 @@ func CommitRows(rows []CommitRow) templ.Component { | |||
| 1109 | 1269 | }() | |
| 1110 | 1270 | } | |
| 1111 | 1271 | ctx = templ.InitializeContext(ctx) | |
| 1112 | - | templ_7745c5c3_Var56 := templ.GetChildren(ctx) | |
| 1113 | - | if templ_7745c5c3_Var56 == nil { | |
| 1114 | - | templ_7745c5c3_Var56 = templ.NopComponent | |
| 1272 | + | templ_7745c5c3_Var65 := templ.GetChildren(ctx) | |
| 1273 | + | if templ_7745c5c3_Var65 == nil { | |
| 1274 | + | templ_7745c5c3_Var65 = templ.NopComponent | |
| 1115 | 1275 | } | |
| 1116 | 1276 | ctx = templ.ClearChildren(ctx) | |
| 1117 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "<table class=\"list commits\" aria-label=\"Commits\"><tbody data-rows>") | |
| 1277 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "<table class=\"list commits\" aria-label=\"Commits\"><tbody data-rows>") | |
| 1118 | 1278 | if templ_7745c5c3_Err != nil { | |
| 1119 | 1279 | return templ_7745c5c3_Err | |
| 1120 | 1280 | } | |
| 1121 | 1281 | for _, c := range rows { | |
| 1122 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<tr") | |
| 1282 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<tr") | |
| 1123 | 1283 | if templ_7745c5c3_Err != nil { | |
| 1124 | 1284 | return templ_7745c5c3_Err | |
| 1125 | 1285 | } | |
| 1126 | 1286 | if c.ID != "" { | |
| 1127 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " id=\"") | |
| 1287 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, " id=\"") | |
| 1128 | 1288 | if templ_7745c5c3_Err != nil { | |
| 1129 | 1289 | return templ_7745c5c3_Err | |
| 1130 | 1290 | } | |
| 1131 | - | var templ_7745c5c3_Var57 string | |
| 1132 | - | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.ID) | |
| 1291 | + | var templ_7745c5c3_Var66 string | |
| 1292 | + | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.ID) | |
| 1133 | 1293 | if templ_7745c5c3_Err != nil { | |
| 1134 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 186, Col: 15} | |
| 1294 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 204, Col: 15} | |
| 1135 | 1295 | } | |
| 1136 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var57) | |
| 1296 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66) | |
| 1137 | 1297 | if templ_7745c5c3_Err != nil { | |
| 1138 | 1298 | return templ_7745c5c3_Err | |
| 1139 | 1299 | } | |
| 1140 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"") | |
| 1300 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\"") | |
| 1141 | 1301 | if templ_7745c5c3_Err != nil { | |
| 1142 | 1302 | return templ_7745c5c3_Err | |
| 1143 | 1303 | } | |
| 1144 | 1304 | } | |
| 1145 | 1305 | if c.Cursor { | |
| 1146 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " data-cursor") | |
| 1306 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, " data-cursor") | |
| 1147 | 1307 | if templ_7745c5c3_Err != nil { | |
| 1148 | 1308 | return templ_7745c5c3_Err | |
| 1149 | 1309 | } | |
| 1150 | 1310 | } | |
| 1151 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "><td class=\"c-sha mute\">") | |
| 1311 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "><td class=\"c-sha mute\">") | |
| 1152 | 1312 | if templ_7745c5c3_Err != nil { | |
| 1153 | 1313 | return templ_7745c5c3_Err | |
| 1154 | 1314 | } | |
| 1155 | - | var templ_7745c5c3_Var58 string | |
| 1156 | - | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 1315 | + | var templ_7745c5c3_Var67 string | |
| 1316 | + | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 1157 | 1317 | if templ_7745c5c3_Err != nil { | |
| 1158 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 190, Col: 37} | |
| 1318 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 208, Col: 37} | |
| 1159 | 1319 | } | |
| 1160 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) | |
| 1320 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67)) | |
| 1161 | 1321 | if templ_7745c5c3_Err != nil { | |
| 1162 | 1322 | return templ_7745c5c3_Err | |
| 1163 | 1323 | } | |
| 1164 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "</td><td class=\"c-subject\"><a href=\"") | |
| 1324 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "</td><td class=\"c-subject\"><a href=\"") | |
| 1165 | 1325 | if templ_7745c5c3_Err != nil { | |
| 1166 | 1326 | return templ_7745c5c3_Err | |
| 1167 | 1327 | } | |
| 1168 | - | var templ_7745c5c3_Var59 templ.SafeURL | |
| 1169 | - | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 1328 | + | var templ_7745c5c3_Var68 templ.SafeURL | |
| 1329 | + | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 1170 | 1330 | if templ_7745c5c3_Err != nil { | |
| 1171 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 192, Col: 38} | |
| 1331 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 210, Col: 38} | |
| 1172 | 1332 | } | |
| 1173 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) | |
| 1333 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | |
| 1174 | 1334 | if templ_7745c5c3_Err != nil { | |
| 1175 | 1335 | return templ_7745c5c3_Err | |
| 1176 | 1336 | } | |
| 1177 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "\">") | |
| 1337 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\">") | |
| 1178 | 1338 | if templ_7745c5c3_Err != nil { | |
| 1179 | 1339 | return templ_7745c5c3_Err | |
| 1180 | 1340 | } | |
| 1181 | - | var templ_7745c5c3_Var60 string | |
| 1182 | - | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 1341 | + | var templ_7745c5c3_Var69 string | |
| 1342 | + | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 1183 | 1343 | if templ_7745c5c3_Err != nil { | |
| 1184 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 192, Col: 52} | |
| 1344 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 210, Col: 52} | |
| 1185 | 1345 | } | |
| 1186 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) | |
| 1346 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | |
| 1187 | 1347 | if templ_7745c5c3_Err != nil { | |
| 1188 | 1348 | return templ_7745c5c3_Err | |
| 1189 | 1349 | } | |
| 1190 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</a> ") | |
| 1350 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</a> ") | |
| 1191 | 1351 | if templ_7745c5c3_Err != nil { | |
| 1192 | 1352 | return templ_7745c5c3_Err | |
| 1193 | 1353 | } | |
| 1194 | 1354 | if c.Merge { | |
| 1195 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<span class=\"pill\">merge</span> ") | |
| 1355 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<span class=\"pill\">merge</span> ") | |
| 1196 | 1356 | if templ_7745c5c3_Err != nil { | |
| 1197 | 1357 | return templ_7745c5c3_Err | |
| 1198 | 1358 | } | |
| @@ -1204,44 +1364,44 @@ func CommitRows(rows []CommitRow) templ.Component { | |||
| 1204 | 1364 | } | |
| 1205 | 1365 | } | |
| 1206 | 1366 | for _, b := range c.Branches { | |
| 1207 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "<span class=\"pill\">") | |
| 1367 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<span class=\"pill\">") | |
| 1208 | 1368 | if templ_7745c5c3_Err != nil { | |
| 1209 | 1369 | return templ_7745c5c3_Err | |
| 1210 | 1370 | } | |
| 1211 | - | var templ_7745c5c3_Var61 string | |
| 1212 | - | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(b) | |
| 1371 | + | var templ_7745c5c3_Var70 string | |
| 1372 | + | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(b) | |
| 1213 | 1373 | if templ_7745c5c3_Err != nil { | |
| 1214 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 200, Col: 29} | |
| 1374 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 218, Col: 29} | |
| 1215 | 1375 | } | |
| 1216 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | |
| 1376 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | |
| 1217 | 1377 | if templ_7745c5c3_Err != nil { | |
| 1218 | 1378 | return templ_7745c5c3_Err | |
| 1219 | 1379 | } | |
| 1220 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</span> ") | |
| 1380 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</span> ") | |
| 1221 | 1381 | if templ_7745c5c3_Err != nil { | |
| 1222 | 1382 | return templ_7745c5c3_Err | |
| 1223 | 1383 | } | |
| 1224 | 1384 | } | |
| 1225 | 1385 | for _, t := range c.Tags { | |
| 1226 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<span class=\"pill tag\"><span class=\"sr\">tag </span>") | |
| 1386 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "<span class=\"pill tag\"><span class=\"sr\">tag </span>") | |
| 1227 | 1387 | if templ_7745c5c3_Err != nil { | |
| 1228 | 1388 | return templ_7745c5c3_Err | |
| 1229 | 1389 | } | |
| 1230 | - | var templ_7745c5c3_Var62 string | |
| 1231 | - | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(t) | |
| 1390 | + | var templ_7745c5c3_Var71 string | |
| 1391 | + | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(t) | |
| 1232 | 1392 | if templ_7745c5c3_Err != nil { | |
| 1233 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 203, Col: 61} | |
| 1393 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 221, Col: 61} | |
| 1234 | 1394 | } | |
| 1235 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | |
| 1395 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | |
| 1236 | 1396 | if templ_7745c5c3_Err != nil { | |
| 1237 | 1397 | return templ_7745c5c3_Err | |
| 1238 | 1398 | } | |
| 1239 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</span>") | |
| 1399 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "</span>") | |
| 1240 | 1400 | if templ_7745c5c3_Err != nil { | |
| 1241 | 1401 | return templ_7745c5c3_Err | |
| 1242 | 1402 | } | |
| 1243 | 1403 | } | |
| 1244 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "</td><td class=\"c-ck\">") | |
| 1404 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "</td><td class=\"c-ck\">") | |
| 1245 | 1405 | if templ_7745c5c3_Err != nil { | |
| 1246 | 1406 | return templ_7745c5c3_Err | |
| 1247 | 1407 | } | |
| @@ -1249,64 +1409,64 @@ func CommitRows(rows []CommitRow) templ.Component { | |||
| 1249 | 1409 | if templ_7745c5c3_Err != nil { | |
| 1250 | 1410 | return templ_7745c5c3_Err | |
| 1251 | 1411 | } | |
| 1252 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</td><td class=\"c-who dim\">") | |
| 1412 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "</td><td class=\"c-who dim\">") | |
| 1253 | 1413 | if templ_7745c5c3_Err != nil { | |
| 1254 | 1414 | return templ_7745c5c3_Err | |
| 1255 | 1415 | } | |
| 1256 | - | var templ_7745c5c3_Var63 string | |
| 1257 | - | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1416 | + | var templ_7745c5c3_Var72 string | |
| 1417 | + | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1258 | 1418 | if templ_7745c5c3_Err != nil { | |
| 1259 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 207, Col: 37} | |
| 1419 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 225, Col: 37} | |
| 1260 | 1420 | } | |
| 1261 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) | |
| 1421 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) | |
| 1262 | 1422 | if templ_7745c5c3_Err != nil { | |
| 1263 | 1423 | return templ_7745c5c3_Err | |
| 1264 | 1424 | } | |
| 1265 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</td><td class=\"c-when nowrap dim\"><time datetime=\"") | |
| 1425 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</td><td class=\"c-when nowrap dim\"><time datetime=\"") | |
| 1266 | 1426 | if templ_7745c5c3_Err != nil { | |
| 1267 | 1427 | return templ_7745c5c3_Err | |
| 1268 | 1428 | } | |
| 1269 | - | var templ_7745c5c3_Var64 string | |
| 1270 | - | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1429 | + | var templ_7745c5c3_Var73 string | |
| 1430 | + | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1271 | 1431 | if templ_7745c5c3_Err != nil { | |
| 1272 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 208, Col: 60} | |
| 1432 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 226, Col: 60} | |
| 1273 | 1433 | } | |
| 1274 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64) | |
| 1434 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var73) | |
| 1275 | 1435 | if templ_7745c5c3_Err != nil { | |
| 1276 | 1436 | return templ_7745c5c3_Err | |
| 1277 | 1437 | } | |
| 1278 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\" title=\"") | |
| 1438 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "\" title=\"") | |
| 1279 | 1439 | if templ_7745c5c3_Err != nil { | |
| 1280 | 1440 | return templ_7745c5c3_Err | |
| 1281 | 1441 | } | |
| 1282 | - | var templ_7745c5c3_Var65 string | |
| 1283 | - | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1442 | + | var templ_7745c5c3_Var74 string | |
| 1443 | + | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1284 | 1444 | if templ_7745c5c3_Err != nil { | |
| 1285 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 208, Col: 79} | |
| 1445 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 226, Col: 79} | |
| 1286 | 1446 | } | |
| 1287 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var65) | |
| 1447 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74) | |
| 1288 | 1448 | if templ_7745c5c3_Err != nil { | |
| 1289 | 1449 | return templ_7745c5c3_Err | |
| 1290 | 1450 | } | |
| 1291 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "\">") | |
| 1451 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "\">") | |
| 1292 | 1452 | if templ_7745c5c3_Err != nil { | |
| 1293 | 1453 | return templ_7745c5c3_Err | |
| 1294 | 1454 | } | |
| 1295 | - | var templ_7745c5c3_Var66 string | |
| 1296 | - | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1455 | + | var templ_7745c5c3_Var75 string | |
| 1456 | + | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1297 | 1457 | if templ_7745c5c3_Err != nil { | |
| 1298 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 208, Col: 90} | |
| 1458 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 226, Col: 90} | |
| 1299 | 1459 | } | |
| 1300 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) | |
| 1460 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) | |
| 1301 | 1461 | if templ_7745c5c3_Err != nil { | |
| 1302 | 1462 | return templ_7745c5c3_Err | |
| 1303 | 1463 | } | |
| 1304 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</time></td></tr>") | |
| 1464 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "</time></td></tr>") | |
| 1305 | 1465 | if templ_7745c5c3_Err != nil { | |
| 1306 | 1466 | return templ_7745c5c3_Err | |
| 1307 | 1467 | } | |
| 1308 | 1468 | } | |
| 1309 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</tbody></table>") | |
| 1469 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "</tbody></table>") | |
| 1310 | 1470 | if templ_7745c5c3_Err != nil { | |
| 1311 | 1471 | return templ_7745c5c3_Err | |
| 1312 | 1472 | } | |
| @@ -1332,75 +1492,75 @@ func signaturePill(b *SignatureBadge) templ.Component { | |||
| 1332 | 1492 | }() | |
| 1333 | 1493 | } | |
| 1334 | 1494 | ctx = templ.InitializeContext(ctx) | |
| 1335 | - | templ_7745c5c3_Var67 := templ.GetChildren(ctx) | |
| 1336 | - | if templ_7745c5c3_Var67 == nil { | |
| 1337 | - | templ_7745c5c3_Var67 = templ.NopComponent | |
| 1495 | + | templ_7745c5c3_Var76 := templ.GetChildren(ctx) | |
| 1496 | + | if templ_7745c5c3_Var76 == nil { | |
| 1497 | + | templ_7745c5c3_Var76 = templ.NopComponent | |
| 1338 | 1498 | } | |
| 1339 | 1499 | ctx = templ.ClearChildren(ctx) | |
| 1340 | - | var templ_7745c5c3_Var68 = []any{"pill sig", b.Class} | |
| 1341 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var68...) | |
| 1500 | + | var templ_7745c5c3_Var77 = []any{"pill sig", b.Class} | |
| 1501 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var77...) | |
| 1342 | 1502 | if templ_7745c5c3_Err != nil { | |
| 1343 | 1503 | return templ_7745c5c3_Err | |
| 1344 | 1504 | } | |
| 1345 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "<span class=\"") | |
| 1505 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "<span class=\"") | |
| 1346 | 1506 | if templ_7745c5c3_Err != nil { | |
| 1347 | 1507 | return templ_7745c5c3_Err | |
| 1348 | 1508 | } | |
| 1349 | - | var templ_7745c5c3_Var69 string | |
| 1350 | - | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var68).String()) | |
| 1509 | + | var templ_7745c5c3_Var78 string | |
| 1510 | + | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var77).String()) | |
| 1351 | 1511 | if templ_7745c5c3_Err != nil { | |
| 1352 | 1512 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 1, Col: 0} | |
| 1353 | 1513 | } | |
| 1354 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69) | |
| 1514 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var78) | |
| 1355 | 1515 | if templ_7745c5c3_Err != nil { | |
| 1356 | 1516 | return templ_7745c5c3_Err | |
| 1357 | 1517 | } | |
| 1358 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "\" title=\"") | |
| 1518 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "\" title=\"") | |
| 1359 | 1519 | if templ_7745c5c3_Err != nil { | |
| 1360 | 1520 | return templ_7745c5c3_Err | |
| 1361 | 1521 | } | |
| 1362 | - | var templ_7745c5c3_Var70 string | |
| 1363 | - | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.ResolveAttributeValue(b.Title) | |
| 1522 | + | var templ_7745c5c3_Var79 string | |
| 1523 | + | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(b.Title) | |
| 1364 | 1524 | if templ_7745c5c3_Err != nil { | |
| 1365 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 218, Col: 52} | |
| 1525 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 236, Col: 52} | |
| 1366 | 1526 | } | |
| 1367 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var70) | |
| 1527 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79) | |
| 1368 | 1528 | if templ_7745c5c3_Err != nil { | |
| 1369 | 1529 | return templ_7745c5c3_Err | |
| 1370 | 1530 | } | |
| 1371 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\">") | |
| 1531 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "\">") | |
| 1372 | 1532 | if templ_7745c5c3_Err != nil { | |
| 1373 | 1533 | return templ_7745c5c3_Err | |
| 1374 | 1534 | } | |
| 1375 | 1535 | if b.Proto != "" { | |
| 1376 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<span class=\"sig-proto\">") | |
| 1536 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<span class=\"sig-proto\">") | |
| 1377 | 1537 | if templ_7745c5c3_Err != nil { | |
| 1378 | 1538 | return templ_7745c5c3_Err | |
| 1379 | 1539 | } | |
| 1380 | - | var templ_7745c5c3_Var71 string | |
| 1381 | - | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(b.Proto) | |
| 1540 | + | var templ_7745c5c3_Var80 string | |
| 1541 | + | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.JoinStringErrs(b.Proto) | |
| 1382 | 1542 | if templ_7745c5c3_Err != nil { | |
| 1383 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 220, Col: 36} | |
| 1543 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 238, Col: 36} | |
| 1384 | 1544 | } | |
| 1385 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | |
| 1545 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var80)) | |
| 1386 | 1546 | if templ_7745c5c3_Err != nil { | |
| 1387 | 1547 | return templ_7745c5c3_Err | |
| 1388 | 1548 | } | |
| 1389 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</span> ") | |
| 1549 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "</span> ") | |
| 1390 | 1550 | if templ_7745c5c3_Err != nil { | |
| 1391 | 1551 | return templ_7745c5c3_Err | |
| 1392 | 1552 | } | |
| 1393 | 1553 | } | |
| 1394 | - | var templ_7745c5c3_Var72 string | |
| 1395 | - | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label) | |
| 1554 | + | var templ_7745c5c3_Var81 string | |
| 1555 | + | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label) | |
| 1396 | 1556 | if templ_7745c5c3_Err != nil { | |
| 1397 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 222, Col: 11} | |
| 1557 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 240, Col: 11} | |
| 1398 | 1558 | } | |
| 1399 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) | |
| 1559 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) | |
| 1400 | 1560 | if templ_7745c5c3_Err != nil { | |
| 1401 | 1561 | return templ_7745c5c3_Err | |
| 1402 | 1562 | } | |
| 1403 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "</span>") | |
| 1563 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "</span>") | |
| 1404 | 1564 | if templ_7745c5c3_Err != nil { | |
| 1405 | 1565 | return templ_7745c5c3_Err | |
| 1406 | 1566 | } | |
| @@ -231,89 +231,179 @@ func HomeRowView(r HomeRow) templ.Component { | |||||
| 231 | if templ_7745c5c3_Err != nil { | 231 | if templ_7745c5c3_Err != nil { | ||
| 232 | return templ_7745c5c3_Err | 232 | return templ_7745c5c3_Err | ||
| 233 | } | 233 | } | ||
| 234 | + | if rel := r.Release; rel != nil { | |||
| 235 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<a class=\"pill repo-release\" href=\"") | |||
| 236 | + | if templ_7745c5c3_Err != nil { | |||
| 237 | + | return templ_7745c5c3_Err | |||
| 238 | + | } | |||
| 239 | + | var templ_7745c5c3_Var11 templ.SafeURL | |||
| 240 | + | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(rel.Href)) | |||
| 241 | + | if templ_7745c5c3_Err != nil { | |||
| 242 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 45, Col: 64} | |||
| 243 | + | } | |||
| 244 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | |||
| 245 | + | if templ_7745c5c3_Err != nil { | |||
| 246 | + | return templ_7745c5c3_Err | |||
| 247 | + | } | |||
| 248 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" title=\"") | |||
| 249 | + | if templ_7745c5c3_Err != nil { | |||
| 250 | + | return templ_7745c5c3_Err | |||
| 251 | + | } | |||
| 252 | + | var templ_7745c5c3_Var12 string | |||
| 253 | + | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue("Latest release: " + rel.Tag) | |||
| 254 | + | if templ_7745c5c3_Err != nil { | |||
| 255 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 45, Col: 103} | |||
| 256 | + | } | |||
| 257 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12) | |||
| 258 | + | if templ_7745c5c3_Err != nil { | |||
| 259 | + | return templ_7745c5c3_Err | |||
| 260 | + | } | |||
| 261 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">") | |||
| 262 | + | if templ_7745c5c3_Err != nil { | |||
| 263 | + | return templ_7745c5c3_Err | |||
| 264 | + | } | |||
| 265 | + | var templ_7745c5c3_Var13 string | |||
| 266 | + | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(rel.Tag) | |||
| 267 | + | if templ_7745c5c3_Err != nil { | |||
| 268 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 45, Col: 115} | |||
| 269 | + | } | |||
| 270 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | |||
| 271 | + | if templ_7745c5c3_Err != nil { | |||
| 272 | + | return templ_7745c5c3_Err | |||
| 273 | + | } | |||
| 274 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</a> ") | |||
| 275 | + | if templ_7745c5c3_Err != nil { | |||
| 276 | + | return templ_7745c5c3_Err | |||
| 277 | + | } | |||
| 278 | + | } | |||
| 234 | if r.Private { | 279 | if r.Private { | ||
| 235 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"pill\">private</span> ") | 280 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"pill\">private</span> ") |
| 281 | + | if templ_7745c5c3_Err != nil { | |||
| 282 | + | return templ_7745c5c3_Err | |||
| 283 | + | } | |||
| 284 | + | } | |||
| 285 | + | if lic := r.License; lic != nil { | |||
| 286 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<a class=\"pill repo-license\" href=\"") | |||
| 287 | + | if templ_7745c5c3_Err != nil { | |||
| 288 | + | return templ_7745c5c3_Err | |||
| 289 | + | } | |||
| 290 | + | var templ_7745c5c3_Var14 templ.SafeURL | |||
| 291 | + | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(lic.Href)) | |||
| 292 | + | if templ_7745c5c3_Err != nil { | |||
| 293 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 51, Col: 64} | |||
| 294 | + | } | |||
| 295 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | |||
| 296 | + | if templ_7745c5c3_Err != nil { | |||
| 297 | + | return templ_7745c5c3_Err | |||
| 298 | + | } | |||
| 299 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" title=\"") | |||
| 300 | + | if templ_7745c5c3_Err != nil { | |||
| 301 | + | return templ_7745c5c3_Err | |||
| 302 | + | } | |||
| 303 | + | var templ_7745c5c3_Var15 string | |||
| 304 | + | templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("License: " + lic.ID) | |||
| 305 | + | if templ_7745c5c3_Err != nil { | |||
| 306 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 51, Col: 95} | |||
| 307 | + | } | |||
| 308 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15) | |||
| 309 | + | if templ_7745c5c3_Err != nil { | |||
| 310 | + | return templ_7745c5c3_Err | |||
| 311 | + | } | |||
| 312 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">") | |||
| 313 | + | if templ_7745c5c3_Err != nil { | |||
| 314 | + | return templ_7745c5c3_Err | |||
| 315 | + | } | |||
| 316 | + | var templ_7745c5c3_Var16 string | |||
| 317 | + | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(lic.ID) | |||
| 318 | + | if templ_7745c5c3_Err != nil { | |||
| 319 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 51, Col: 106} | |||
| 320 | + | } | |||
| 321 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | |||
| 322 | + | if templ_7745c5c3_Err != nil { | |||
| 323 | + | return templ_7745c5c3_Err | |||
| 324 | + | } | |||
| 325 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</a> ") | |||
| 236 | if templ_7745c5c3_Err != nil { | 326 | if templ_7745c5c3_Err != nil { | ||
| 237 | return templ_7745c5c3_Err | 327 | return templ_7745c5c3_Err | ||
| 238 | } | 328 | } | ||
| 239 | } | 329 | } | ||
| 240 | if r.Fresh { | 330 | if r.Fresh { | ||
| 241 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"pill new\">new activity</span>") | 331 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<span class=\"repo-activity\" title=\"new activity\"><span class=\"ico i-refresh-cw\" aria-hidden=\"true\"></span><span class=\"sr\">new activity</span></span>") |
| 242 | if templ_7745c5c3_Err != nil { | 332 | if templ_7745c5c3_Err != nil { | ||
| 243 | return templ_7745c5c3_Err | 333 | return templ_7745c5c3_Err | ||
| 244 | } | 334 | } | ||
| 245 | } | 335 | } | ||
| 246 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</h2>") | 336 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</h2>") |
| 247 | if templ_7745c5c3_Err != nil { | 337 | if templ_7745c5c3_Err != nil { | ||
| 248 | return templ_7745c5c3_Err | 338 | return templ_7745c5c3_Err | ||
| 249 | } | 339 | } | ||
| 250 | if r.Desc != "" { | 340 | if r.Desc != "" { | ||
| 251 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p class=\"repo-desc\">") | 341 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<p class=\"repo-desc\">") |
| 252 | if templ_7745c5c3_Err != nil { | 342 | if templ_7745c5c3_Err != nil { | ||
| 253 | return templ_7745c5c3_Err | 343 | return templ_7745c5c3_Err | ||
| 254 | } | 344 | } | ||
| 255 | - | var templ_7745c5c3_Var11 string | 345 | + | var templ_7745c5c3_Var17 string |
| 256 | - | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.Desc) | 346 | + | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(r.Desc) |
| 257 | if templ_7745c5c3_Err != nil { | 347 | if templ_7745c5c3_Err != nil { | ||
| 258 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 52, Col: 33} | 348 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 58, Col: 33} |
| 259 | } | 349 | } | ||
| 260 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | 350 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) |
| 261 | if templ_7745c5c3_Err != nil { | 351 | if templ_7745c5c3_Err != nil { | ||
| 262 | return templ_7745c5c3_Err | 352 | return templ_7745c5c3_Err | ||
| 263 | } | 353 | } | ||
| 264 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p>") | 354 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</p>") |
| 265 | if templ_7745c5c3_Err != nil { | 355 | if templ_7745c5c3_Err != nil { | ||
| 266 | return templ_7745c5c3_Err | 356 | return templ_7745c5c3_Err | ||
| 267 | } | 357 | } | ||
| 268 | } else { | 358 | } else { | ||
| 269 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p class=\"repo-desc mute\">no description</p>") | 359 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<p class=\"repo-desc mute\">no description</p>") |
| 270 | if templ_7745c5c3_Err != nil { | 360 | if templ_7745c5c3_Err != nil { | ||
| 271 | return templ_7745c5c3_Err | 361 | return templ_7745c5c3_Err | ||
| 272 | } | 362 | } | ||
| 273 | } | 363 | } | ||
| 274 | if len(r.Topics) > 0 { | 364 | if len(r.Topics) > 0 { | ||
| 275 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<ul class=\"repo-topics\" aria-label=\"") | 365 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<ul class=\"repo-topics\" aria-label=\"") |
| 276 | if templ_7745c5c3_Err != nil { | 366 | if templ_7745c5c3_Err != nil { | ||
| 277 | return templ_7745c5c3_Err | 367 | return templ_7745c5c3_Err | ||
| 278 | } | 368 | } | ||
| 279 | - | var templ_7745c5c3_Var12 string | 369 | + | var templ_7745c5c3_Var18 string |
| 280 | - | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue("Topics of " + r.Name) | 370 | + | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("Topics of " + r.Name) |
| 281 | if templ_7745c5c3_Err != nil { | 371 | if templ_7745c5c3_Err != nil { | ||
| 282 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 57, Col: 62} | 372 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 63, Col: 62} |
| 283 | } | 373 | } | ||
| 284 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12) | 374 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) |
| 285 | if templ_7745c5c3_Err != nil { | 375 | if templ_7745c5c3_Err != nil { | ||
| 286 | return templ_7745c5c3_Err | 376 | return templ_7745c5c3_Err | ||
| 287 | } | 377 | } | ||
| 288 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" data-fit>") | 378 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" data-fit>") |
| 289 | if templ_7745c5c3_Err != nil { | 379 | if templ_7745c5c3_Err != nil { | ||
| 290 | return templ_7745c5c3_Err | 380 | return templ_7745c5c3_Err | ||
| 291 | } | 381 | } | ||
| 292 | for _, t := range r.Topics { | 382 | for _, t := range r.Topics { | ||
| 293 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<li>") | 383 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<li>") |
| 294 | if templ_7745c5c3_Err != nil { | 384 | if templ_7745c5c3_Err != nil { | ||
| 295 | return templ_7745c5c3_Err | 385 | return templ_7745c5c3_Err | ||
| 296 | } | 386 | } | ||
| 297 | - | var templ_7745c5c3_Var13 string | 387 | + | var templ_7745c5c3_Var19 string |
| 298 | - | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(t) | 388 | + | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(t) |
| 299 | if templ_7745c5c3_Err != nil { | 389 | if templ_7745c5c3_Err != nil { | ||
| 300 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 59, Col: 13} | 390 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 65, Col: 13} |
| 301 | } | 391 | } | ||
| 302 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | 392 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) |
| 303 | if templ_7745c5c3_Err != nil { | 393 | if templ_7745c5c3_Err != nil { | ||
| 304 | return templ_7745c5c3_Err | 394 | return templ_7745c5c3_Err | ||
| 305 | } | 395 | } | ||
| 306 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</li>") | 396 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</li>") |
| 307 | if templ_7745c5c3_Err != nil { | 397 | if templ_7745c5c3_Err != nil { | ||
| 308 | return templ_7745c5c3_Err | 398 | return templ_7745c5c3_Err | ||
| 309 | } | 399 | } | ||
| 310 | } | 400 | } | ||
| 311 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</ul>") | 401 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</ul>") |
| 312 | if templ_7745c5c3_Err != nil { | 402 | if templ_7745c5c3_Err != nil { | ||
| 313 | return templ_7745c5c3_Err | 403 | return templ_7745c5c3_Err | ||
| 314 | } | 404 | } | ||
| 315 | } else { | 405 | } else { | ||
| 316 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"repo-topics\" aria-hidden=\"true\"></div>") | 406 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"repo-topics\" aria-hidden=\"true\"></div>") |
| 317 | if templ_7745c5c3_Err != nil { | 407 | if templ_7745c5c3_Err != nil { | ||
| 318 | return templ_7745c5c3_Err | 408 | return templ_7745c5c3_Err | ||
| 319 | } | 409 | } | ||
| @@ -324,425 +414,425 @@ func HomeRowView(r HomeRow) templ.Component { | |||||
| 324 | return templ_7745c5c3_Err | 414 | return templ_7745c5c3_Err | ||
| 325 | } | 415 | } | ||
| 326 | } else { | 416 | } else { | ||
| 327 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div class=\"repo-pulse\" aria-hidden=\"true\"></div>") | 417 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"repo-pulse\" aria-hidden=\"true\"></div>") |
| 328 | if templ_7745c5c3_Err != nil { | 418 | if templ_7745c5c3_Err != nil { | ||
| 329 | return templ_7745c5c3_Err | 419 | return templ_7745c5c3_Err | ||
| 330 | } | 420 | } | ||
| 331 | } | 421 | } | ||
| 332 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><div class=\"repo-head\"><dl class=\"repo-stats\" aria-label=\"") | 422 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</div><div class=\"repo-head\"><dl class=\"repo-stats\" aria-label=\"") |
| 333 | if templ_7745c5c3_Err != nil { | 423 | if templ_7745c5c3_Err != nil { | ||
| 334 | return templ_7745c5c3_Err | 424 | return templ_7745c5c3_Err | ||
| 335 | } | 425 | } | ||
| 336 | - | var templ_7745c5c3_Var14 string | 426 | + | var templ_7745c5c3_Var20 string |
| 337 | - | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue("Figures of " + r.Name) | 427 | + | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue("Figures of " + r.Name) |
| 338 | if templ_7745c5c3_Err != nil { | 428 | if templ_7745c5c3_Err != nil { | ||
| 339 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 72, Col: 61} | 429 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 78, Col: 61} |
| 340 | } | 430 | } | ||
| 341 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14) | 431 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20) |
| 342 | if templ_7745c5c3_Err != nil { | 432 | if templ_7745c5c3_Err != nil { | ||
| 343 | return templ_7745c5c3_Err | 433 | return templ_7745c5c3_Err | ||
| 344 | } | 434 | } | ||
| 345 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">") | 435 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\">") |
| 346 | if templ_7745c5c3_Err != nil { | 436 | if templ_7745c5c3_Err != nil { | ||
| 347 | return templ_7745c5c3_Err | 437 | return templ_7745c5c3_Err | ||
| 348 | } | 438 | } | ||
| 349 | if s := r.Stats; s != nil { | 439 | if s := r.Stats; s != nil { | ||
| 350 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div><dt>commits</dt><dd>") | 440 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<div><dt>commits</dt><dd>") |
| 351 | if templ_7745c5c3_Err != nil { | 441 | if templ_7745c5c3_Err != nil { | ||
| 352 | return templ_7745c5c3_Err | 442 | return templ_7745c5c3_Err | ||
| 353 | } | 443 | } | ||
| 354 | - | var templ_7745c5c3_Var15 string | 444 | + | var templ_7745c5c3_Var21 string |
| 355 | - | templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(s.Commits) | 445 | + | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(s.Commits) |
| 356 | if templ_7745c5c3_Err != nil { | 446 | if templ_7745c5c3_Err != nil { | ||
| 357 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 74, Col: 41} | 447 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 80, Col: 41} |
| 358 | } | 448 | } | ||
| 359 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) | 449 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) |
| 360 | if templ_7745c5c3_Err != nil { | 450 | if templ_7745c5c3_Err != nil { | ||
| 361 | return templ_7745c5c3_Err | 451 | return templ_7745c5c3_Err | ||
| 362 | } | 452 | } | ||
| 363 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</dd></div><div><dt>last 30 days</dt><dd>") | 453 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</dd></div><div><dt>last 30 days</dt><dd>") |
| 364 | if templ_7745c5c3_Err != nil { | 454 | if templ_7745c5c3_Err != nil { | ||
| 365 | return templ_7745c5c3_Err | 455 | return templ_7745c5c3_Err | ||
| 366 | } | 456 | } | ||
| 367 | - | var templ_7745c5c3_Var16 string | 457 | + | var templ_7745c5c3_Var22 string |
| 368 | - | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(s.Recent) | 458 | + | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(s.Recent) |
| 369 | if templ_7745c5c3_Err != nil { | 459 | if templ_7745c5c3_Err != nil { | ||
| 370 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 75, Col: 45} | 460 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 81, Col: 45} |
| 371 | } | 461 | } | ||
| 372 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | 462 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) |
| 373 | if templ_7745c5c3_Err != nil { | 463 | if templ_7745c5c3_Err != nil { | ||
| 374 | return templ_7745c5c3_Err | 464 | return templ_7745c5c3_Err | ||
| 375 | } | 465 | } | ||
| 376 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</dd></div>") | 466 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</dd></div>") |
| 377 | if templ_7745c5c3_Err != nil { | 467 | if templ_7745c5c3_Err != nil { | ||
| 378 | return templ_7745c5c3_Err | 468 | return templ_7745c5c3_Err | ||
| 379 | } | 469 | } | ||
| 380 | } | 470 | } | ||
| 381 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div class=\"rs-active\"><dt>last activity</dt>") | 471 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"rs-active\"><dt>last activity</dt>") |
| 382 | if templ_7745c5c3_Err != nil { | 472 | if templ_7745c5c3_Err != nil { | ||
| 383 | return templ_7745c5c3_Err | 473 | return templ_7745c5c3_Err | ||
| 384 | } | 474 | } | ||
| 385 | if r.ActiveAt != "" { | 475 | if r.ActiveAt != "" { | ||
| 386 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<dd><time datetime=\"") | 476 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<dd><time datetime=\"") |
| 387 | if templ_7745c5c3_Err != nil { | 477 | if templ_7745c5c3_Err != nil { | ||
| 388 | return templ_7745c5c3_Err | 478 | return templ_7745c5c3_Err | ||
| 389 | } | 479 | } | ||
| 390 | - | var templ_7745c5c3_Var17 string | 480 | + | var templ_7745c5c3_Var23 string |
| 391 | - | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ActiveAt) | 481 | + | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ActiveAt) |
| 392 | if templ_7745c5c3_Err != nil { | 482 | if templ_7745c5c3_Err != nil { | ||
| 393 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 80, Col: 37} | 483 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 86, Col: 37} |
| 394 | } | 484 | } | ||
| 395 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) | 485 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) |
| 396 | if templ_7745c5c3_Err != nil { | 486 | if templ_7745c5c3_Err != nil { | ||
| 397 | return templ_7745c5c3_Err | 487 | return templ_7745c5c3_Err | ||
| 398 | } | 488 | } | ||
| 399 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" title=\"") | 489 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" title=\"") |
| 400 | if templ_7745c5c3_Err != nil { | 490 | if templ_7745c5c3_Err != nil { | ||
| 401 | return templ_7745c5c3_Err | 491 | return templ_7745c5c3_Err | ||
| 402 | } | 492 | } | ||
| 403 | - | var templ_7745c5c3_Var18 string | 493 | + | var templ_7745c5c3_Var24 string |
| 404 | - | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ActiveAt) | 494 | + | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ActiveAt) |
| 405 | if templ_7745c5c3_Err != nil { | 495 | if templ_7745c5c3_Err != nil { | ||
| 406 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 80, Col: 58} | 496 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 86, Col: 58} |
| 407 | } | 497 | } | ||
| 408 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) | 498 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) |
| 409 | if templ_7745c5c3_Err != nil { | 499 | if templ_7745c5c3_Err != nil { | ||
| 410 | return templ_7745c5c3_Err | 500 | return templ_7745c5c3_Err | ||
| 411 | } | 501 | } | ||
| 412 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">") | 502 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">") |
| 413 | if templ_7745c5c3_Err != nil { | 503 | if templ_7745c5c3_Err != nil { | ||
| 414 | return templ_7745c5c3_Err | 504 | return templ_7745c5c3_Err | ||
| 415 | } | 505 | } | ||
| 416 | - | var templ_7745c5c3_Var19 string | 506 | + | var templ_7745c5c3_Var25 string |
| 417 | - | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(r.Active) | 507 | + | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(r.Active) |
| 418 | if templ_7745c5c3_Err != nil { | 508 | if templ_7745c5c3_Err != nil { | ||
| 419 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 80, Col: 71} | 509 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 86, Col: 71} |
| 420 | } | 510 | } | ||
| 421 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | 511 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) |
| 422 | if templ_7745c5c3_Err != nil { | 512 | if templ_7745c5c3_Err != nil { | ||
| 423 | return templ_7745c5c3_Err | 513 | return templ_7745c5c3_Err | ||
| 424 | } | 514 | } | ||
| 425 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</time></dd>") | 515 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</time></dd>") |
| 426 | if templ_7745c5c3_Err != nil { | 516 | if templ_7745c5c3_Err != nil { | ||
| 427 | return templ_7745c5c3_Err | 517 | return templ_7745c5c3_Err | ||
| 428 | } | 518 | } | ||
| 429 | } else { | 519 | } else { | ||
| 430 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<dd class=\"mute\">never</dd>") | 520 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<dd class=\"mute\">never</dd>") |
| 431 | if templ_7745c5c3_Err != nil { | 521 | if templ_7745c5c3_Err != nil { | ||
| 432 | return templ_7745c5c3_Err | 522 | return templ_7745c5c3_Err | ||
| 433 | } | 523 | } | ||
| 434 | } | 524 | } | ||
| 435 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div>") | 525 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div>") |
| 436 | if templ_7745c5c3_Err != nil { | 526 | if templ_7745c5c3_Err != nil { | ||
| 437 | return templ_7745c5c3_Err | 527 | return templ_7745c5c3_Err | ||
| 438 | } | 528 | } | ||
| 439 | if s := r.Stats; s != nil && s.Tag != "" { | 529 | if s := r.Stats; s != nil && s.Tag != "" { | ||
| 440 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<div class=\"rs-tag\"><dt>last tag</dt>") | 530 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div class=\"rs-tag\"><dt>last tag</dt>") |
| 441 | if templ_7745c5c3_Err != nil { | 531 | if templ_7745c5c3_Err != nil { | ||
| 442 | return templ_7745c5c3_Err | 532 | return templ_7745c5c3_Err | ||
| 443 | } | 533 | } | ||
| 444 | if s.TagHref != "" { | 534 | if s.TagHref != "" { | ||
| 445 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<dd><a href=\"") | 535 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<dd><a href=\"") |
| 446 | if templ_7745c5c3_Err != nil { | 536 | if templ_7745c5c3_Err != nil { | ||
| 447 | return templ_7745c5c3_Err | 537 | return templ_7745c5c3_Err | ||
| 448 | } | 538 | } | ||
| 449 | - | var templ_7745c5c3_Var20 templ.SafeURL | 539 | + | var templ_7745c5c3_Var26 templ.SafeURL |
| 450 | - | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.TagHref)) | 540 | + | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.TagHref)) |
| 451 | if templ_7745c5c3_Err != nil { | 541 | if templ_7745c5c3_Err != nil { | ||
| 452 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 89, Col: 45} | 542 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 95, Col: 45} |
| 453 | } | 543 | } | ||
| 454 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) | 544 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) |
| 455 | if templ_7745c5c3_Err != nil { | 545 | if templ_7745c5c3_Err != nil { | ||
| 456 | return templ_7745c5c3_Err | 546 | return templ_7745c5c3_Err | ||
| 457 | } | 547 | } | ||
| 458 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" title=\"") | 548 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" title=\"") |
| 459 | if templ_7745c5c3_Err != nil { | 549 | if templ_7745c5c3_Err != nil { | ||
| 460 | return templ_7745c5c3_Err | 550 | return templ_7745c5c3_Err | ||
| 461 | } | 551 | } | ||
| 462 | - | var templ_7745c5c3_Var21 string | 552 | + | var templ_7745c5c3_Var27 string |
| 463 | - | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Tag + " · made " + s.TagWhen) | 553 | + | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Tag + " · made " + s.TagWhen) |
| 464 | if templ_7745c5c3_Err != nil { | 554 | if templ_7745c5c3_Err != nil { | ||
| 465 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 89, Col: 87} | 555 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 95, Col: 87} |
| 466 | } | 556 | } | ||
| 467 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21) | 557 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27) |
| 468 | if templ_7745c5c3_Err != nil { | 558 | if templ_7745c5c3_Err != nil { | ||
| 469 | return templ_7745c5c3_Err | 559 | return templ_7745c5c3_Err | ||
| 470 | } | 560 | } | ||
| 471 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">") | 561 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\">") |
| 472 | if templ_7745c5c3_Err != nil { | 562 | if templ_7745c5c3_Err != nil { | ||
| 473 | return templ_7745c5c3_Err | 563 | return templ_7745c5c3_Err | ||
| 474 | } | 564 | } | ||
| 475 | - | var templ_7745c5c3_Var22 string | 565 | + | var templ_7745c5c3_Var28 string |
| 476 | - | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(s.Tag) | 566 | + | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(s.Tag) |
| 477 | if templ_7745c5c3_Err != nil { | 567 | if templ_7745c5c3_Err != nil { | ||
| 478 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 89, Col: 97} | 568 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 95, Col: 97} |
| 479 | } | 569 | } | ||
| 480 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | 570 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) |
| 481 | if templ_7745c5c3_Err != nil { | 571 | if templ_7745c5c3_Err != nil { | ||
| 482 | return templ_7745c5c3_Err | 572 | return templ_7745c5c3_Err | ||
| 483 | } | 573 | } | ||
| 484 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</a></dd>") | 574 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</a></dd>") |
| 485 | if templ_7745c5c3_Err != nil { | 575 | if templ_7745c5c3_Err != nil { | ||
| 486 | return templ_7745c5c3_Err | 576 | return templ_7745c5c3_Err | ||
| 487 | } | 577 | } | ||
| 488 | } else { | 578 | } else { | ||
| 489 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<dd title=\"") | 579 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<dd title=\"") |
| 490 | if templ_7745c5c3_Err != nil { | 580 | if templ_7745c5c3_Err != nil { | ||
| 491 | return templ_7745c5c3_Err | 581 | return templ_7745c5c3_Err | ||
| 492 | } | 582 | } | ||
| 493 | - | var templ_7745c5c3_Var23 string | 583 | + | var templ_7745c5c3_Var29 string |
| 494 | - | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Tag + " · made " + s.TagWhen) | 584 | + | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Tag + " · made " + s.TagWhen) |
| 495 | if templ_7745c5c3_Err != nil { | 585 | if templ_7745c5c3_Err != nil { | ||
| 496 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 91, Col: 50} | 586 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 97, Col: 50} |
| 497 | } | 587 | } | ||
| 498 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) | 588 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29) |
| 499 | if templ_7745c5c3_Err != nil { | 589 | if templ_7745c5c3_Err != nil { | ||
| 500 | return templ_7745c5c3_Err | 590 | return templ_7745c5c3_Err | ||
| 501 | } | 591 | } | ||
| 502 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">") | 592 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\">") |
| 503 | if templ_7745c5c3_Err != nil { | 593 | if templ_7745c5c3_Err != nil { | ||
| 504 | return templ_7745c5c3_Err | 594 | return templ_7745c5c3_Err | ||
| 505 | } | 595 | } | ||
| 506 | - | var templ_7745c5c3_Var24 string | 596 | + | var templ_7745c5c3_Var30 string |
| 507 | - | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(s.Tag) | 597 | + | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(s.Tag) |
| 508 | if templ_7745c5c3_Err != nil { | 598 | if templ_7745c5c3_Err != nil { | ||
| 509 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 91, Col: 60} | 599 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 97, Col: 60} |
| 510 | } | 600 | } | ||
| 511 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) | 601 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) |
| 512 | if templ_7745c5c3_Err != nil { | 602 | if templ_7745c5c3_Err != nil { | ||
| 513 | return templ_7745c5c3_Err | 603 | return templ_7745c5c3_Err | ||
| 514 | } | 604 | } | ||
| 515 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</dd>") | 605 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</dd>") |
| 516 | if templ_7745c5c3_Err != nil { | 606 | if templ_7745c5c3_Err != nil { | ||
| 517 | return templ_7745c5c3_Err | 607 | return templ_7745c5c3_Err | ||
| 518 | } | 608 | } | ||
| 519 | } | 609 | } | ||
| 520 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</div>") | 610 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div>") |
| 521 | if templ_7745c5c3_Err != nil { | 611 | if templ_7745c5c3_Err != nil { | ||
| 522 | return templ_7745c5c3_Err | 612 | return templ_7745c5c3_Err | ||
| 523 | } | 613 | } | ||
| 524 | } | 614 | } | ||
| 525 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</dl>") | 615 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</dl>") |
| 526 | if templ_7745c5c3_Err != nil { | 616 | if templ_7745c5c3_Err != nil { | ||
| 527 | return templ_7745c5c3_Err | 617 | return templ_7745c5c3_Err | ||
| 528 | } | 618 | } | ||
| 529 | if r.CI != nil { | 619 | if r.CI != nil { | ||
| 530 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<a class=\"repo-ci\" href=\"") | 620 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<a class=\"repo-ci\" href=\"") |
| 531 | if templ_7745c5c3_Err != nil { | 621 | if templ_7745c5c3_Err != nil { | ||
| 532 | return templ_7745c5c3_Err | 622 | return templ_7745c5c3_Err | ||
| 533 | } | 623 | } | ||
| 534 | - | var templ_7745c5c3_Var25 templ.SafeURL | 624 | + | var templ_7745c5c3_Var31 templ.SafeURL |
| 535 | - | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CI.Href)) | 625 | + | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CI.Href)) |
| 536 | if templ_7745c5c3_Err != nil { | 626 | if templ_7745c5c3_Err != nil { | ||
| 537 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 97, Col: 54} | 627 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 103, Col: 54} |
| 538 | } | 628 | } | ||
| 539 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) | 629 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) |
| 540 | if templ_7745c5c3_Err != nil { | 630 | if templ_7745c5c3_Err != nil { | ||
| 541 | return templ_7745c5c3_Err | 631 | return templ_7745c5c3_Err | ||
| 542 | } | 632 | } | ||
| 543 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" title=\"") | 633 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" title=\"") |
| 544 | if templ_7745c5c3_Err != nil { | 634 | if templ_7745c5c3_Err != nil { | ||
| 545 | return templ_7745c5c3_Err | 635 | return templ_7745c5c3_Err | ||
| 546 | } | 636 | } | ||
| 547 | - | var templ_7745c5c3_Var26 string | 637 | + | var templ_7745c5c3_Var32 string |
| 548 | - | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CI.Title) | 638 | + | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CI.Label + " · " + r.CI.Title) |
| 549 | if templ_7745c5c3_Err != nil { | 639 | if templ_7745c5c3_Err != nil { | ||
| 550 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 97, Col: 75} | 640 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 103, Col: 97} |
| 551 | } | 641 | } | ||
| 552 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26) | 642 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) |
| 553 | if templ_7745c5c3_Err != nil { | 643 | if templ_7745c5c3_Err != nil { | ||
| 554 | return templ_7745c5c3_Err | 644 | return templ_7745c5c3_Err | ||
| 555 | } | 645 | } | ||
| 556 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">") | 646 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\">") |
| 557 | if templ_7745c5c3_Err != nil { | 647 | if templ_7745c5c3_Err != nil { | ||
| 558 | return templ_7745c5c3_Err | 648 | return templ_7745c5c3_Err | ||
| 559 | } | 649 | } | ||
| 560 | - | var templ_7745c5c3_Var27 = []any{"ck", "ck-" + r.CI.Kind} | 650 | + | var templ_7745c5c3_Var33 = []any{"ck", "ck-" + r.CI.Kind} |
| 561 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var27...) | 651 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var33...) |
| 562 | if templ_7745c5c3_Err != nil { | 652 | if templ_7745c5c3_Err != nil { | ||
| 563 | return templ_7745c5c3_Err | 653 | return templ_7745c5c3_Err | ||
| 564 | } | 654 | } | ||
| 565 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<span class=\"") | 655 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<span class=\"") |
| 566 | if templ_7745c5c3_Err != nil { | 656 | if templ_7745c5c3_Err != nil { | ||
| 567 | return templ_7745c5c3_Err | 657 | return templ_7745c5c3_Err | ||
| 568 | } | 658 | } | ||
| 569 | - | var templ_7745c5c3_Var28 string | 659 | + | var templ_7745c5c3_Var34 string |
| 570 | - | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var27).String()) | 660 | + | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var33).String()) |
| 571 | if templ_7745c5c3_Err != nil { | 661 | if templ_7745c5c3_Err != nil { | ||
| 572 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 1, Col: 0} | 662 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 1, Col: 0} | ||
| 573 | } | 663 | } | ||
| 574 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) | 664 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34) |
| 575 | if templ_7745c5c3_Err != nil { | 665 | if templ_7745c5c3_Err != nil { | ||
| 576 | return templ_7745c5c3_Err | 666 | return templ_7745c5c3_Err | ||
| 577 | } | 667 | } | ||
| 578 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" aria-hidden=\"true\"></span> <span class=\"sr\">CI: </span><span class=\"ci-label\">") | 668 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" aria-hidden=\"true\"></span> <span class=\"sr\">") |
| 579 | if templ_7745c5c3_Err != nil { | 669 | if templ_7745c5c3_Err != nil { | ||
| 580 | return templ_7745c5c3_Err | 670 | return templ_7745c5c3_Err | ||
| 581 | } | 671 | } | ||
| 582 | - | var templ_7745c5c3_Var29 string | 672 | + | var templ_7745c5c3_Var35 string |
| 583 | - | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(r.CI.Label) | 673 | + | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs("CI: " + r.CI.Label) |
| 584 | if templ_7745c5c3_Err != nil { | 674 | if templ_7745c5c3_Err != nil { | ||
| 585 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 99, Col: 68} | 675 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 105, Col: 43} |
| 586 | } | 676 | } | ||
| 587 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | 677 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) |
| 588 | if templ_7745c5c3_Err != nil { | 678 | if templ_7745c5c3_Err != nil { | ||
| 589 | return templ_7745c5c3_Err | 679 | return templ_7745c5c3_Err | ||
| 590 | } | 680 | } | ||
| 591 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span></a>") | 681 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</span></a>") |
| 592 | if templ_7745c5c3_Err != nil { | 682 | if templ_7745c5c3_Err != nil { | ||
| 593 | return templ_7745c5c3_Err | 683 | return templ_7745c5c3_Err | ||
| 594 | } | 684 | } | ||
| 595 | } | 685 | } | ||
| 596 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div>") | 686 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</div>") |
| 597 | if templ_7745c5c3_Err != nil { | 687 | if templ_7745c5c3_Err != nil { | ||
| 598 | return templ_7745c5c3_Err | 688 | return templ_7745c5c3_Err | ||
| 599 | } | 689 | } | ||
| 600 | if len(r.Commits) > 0 { | 690 | if len(r.Commits) > 0 { | ||
| 601 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<ul class=\"repo-commits\"") | 691 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<ul class=\"repo-commits\"") |
| 602 | if templ_7745c5c3_Err != nil { | 692 | if templ_7745c5c3_Err != nil { | ||
| 603 | return templ_7745c5c3_Err | 693 | return templ_7745c5c3_Err | ||
| 604 | } | 694 | } | ||
| 605 | if len(r.Commits) == homeRecent { | 695 | if len(r.Commits) == homeRecent { | ||
| 606 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " data-full") | 696 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, " data-full") |
| 607 | if templ_7745c5c3_Err != nil { | 697 | if templ_7745c5c3_Err != nil { | ||
| 608 | return templ_7745c5c3_Err | 698 | return templ_7745c5c3_Err | ||
| 609 | } | 699 | } | ||
| 610 | } | 700 | } | ||
| 611 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " aria-label=\"") | 701 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " aria-label=\"") |
| 612 | if templ_7745c5c3_Err != nil { | 702 | if templ_7745c5c3_Err != nil { | ||
| 613 | return templ_7745c5c3_Err | 703 | return templ_7745c5c3_Err | ||
| 614 | } | 704 | } | ||
| 615 | - | var templ_7745c5c3_Var30 string | 705 | + | var templ_7745c5c3_Var36 string |
| 616 | - | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue("Recent commits of " + r.Name) | 706 | + | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue("Recent commits of " + r.Name) |
| 617 | if templ_7745c5c3_Err != nil { | 707 | if templ_7745c5c3_Err != nil { | ||
| 618 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 107, Col: 46} | 708 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 113, Col: 46} |
| 619 | } | 709 | } | ||
| 620 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30) | 710 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36) |
| 621 | if templ_7745c5c3_Err != nil { | 711 | if templ_7745c5c3_Err != nil { | ||
| 622 | return templ_7745c5c3_Err | 712 | return templ_7745c5c3_Err | ||
| 623 | } | 713 | } | ||
| 624 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\">") | 714 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\">") |
| 625 | if templ_7745c5c3_Err != nil { | 715 | if templ_7745c5c3_Err != nil { | ||
| 626 | return templ_7745c5c3_Err | 716 | return templ_7745c5c3_Err | ||
| 627 | } | 717 | } | ||
| 628 | for _, c := range r.Commits { | 718 | for _, c := range r.Commits { | ||
| 629 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<li><a class=\"rc-sha\" href=\"") | 719 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<li><a class=\"rc-sha\" href=\"") |
| 630 | if templ_7745c5c3_Err != nil { | 720 | if templ_7745c5c3_Err != nil { | ||
| 631 | return templ_7745c5c3_Err | 721 | return templ_7745c5c3_Err | ||
| 632 | } | 722 | } | ||
| 633 | - | var templ_7745c5c3_Var31 templ.SafeURL | 723 | + | var templ_7745c5c3_Var37 templ.SafeURL |
| 634 | - | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | 724 | + | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) |
| 635 | if templ_7745c5c3_Err != nil { | 725 | if templ_7745c5c3_Err != nil { | ||
| 636 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 111, Col: 53} | 726 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 117, Col: 53} |
| 637 | } | 727 | } | ||
| 638 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | 728 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) |
| 639 | if templ_7745c5c3_Err != nil { | 729 | if templ_7745c5c3_Err != nil { | ||
| 640 | return templ_7745c5c3_Err | 730 | return templ_7745c5c3_Err | ||
| 641 | } | 731 | } | ||
| 642 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\">") | 732 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\">") |
| 643 | if templ_7745c5c3_Err != nil { | 733 | if templ_7745c5c3_Err != nil { | ||
| 644 | return templ_7745c5c3_Err | 734 | return templ_7745c5c3_Err | ||
| 645 | } | 735 | } | ||
| 646 | - | var templ_7745c5c3_Var32 string | 736 | + | var templ_7745c5c3_Var38 string |
| 647 | - | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | 737 | + | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) |
| 648 | if templ_7745c5c3_Err != nil { | 738 | if templ_7745c5c3_Err != nil { | ||
| 649 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 111, Col: 65} | 739 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 117, Col: 65} |
| 650 | } | 740 | } | ||
| 651 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | 741 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) |
| 652 | if templ_7745c5c3_Err != nil { | 742 | if templ_7745c5c3_Err != nil { | ||
| 653 | return templ_7745c5c3_Err | 743 | return templ_7745c5c3_Err | ||
| 654 | } | 744 | } | ||
| 655 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</a> <span class=\"rc-subject\" title=\"") | 745 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</a> <span class=\"rc-subject\" title=\"") |
| 656 | if templ_7745c5c3_Err != nil { | 746 | if templ_7745c5c3_Err != nil { | ||
| 657 | return templ_7745c5c3_Err | 747 | return templ_7745c5c3_Err | ||
| 658 | } | 748 | } | ||
| 659 | - | var templ_7745c5c3_Var33 string | 749 | + | var templ_7745c5c3_Var39 string |
| 660 | - | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Subject) | 750 | + | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Subject) |
| 661 | if templ_7745c5c3_Err != nil { | 751 | if templ_7745c5c3_Err != nil { | ||
| 662 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 112, Col: 48} | 752 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 118, Col: 48} |
| 663 | } | 753 | } | ||
| 664 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) | 754 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39) |
| 665 | if templ_7745c5c3_Err != nil { | 755 | if templ_7745c5c3_Err != nil { | ||
| 666 | return templ_7745c5c3_Err | 756 | return templ_7745c5c3_Err | ||
| 667 | } | 757 | } | ||
| 668 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">") | 758 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\">") |
| 669 | if templ_7745c5c3_Err != nil { | 759 | if templ_7745c5c3_Err != nil { | ||
| 670 | return templ_7745c5c3_Err | 760 | return templ_7745c5c3_Err | ||
| 671 | } | 761 | } | ||
| 672 | - | var templ_7745c5c3_Var34 string | 762 | + | var templ_7745c5c3_Var40 string |
| 673 | - | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | 763 | + | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) |
| 674 | if templ_7745c5c3_Err != nil { | 764 | if templ_7745c5c3_Err != nil { | ||
| 675 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 112, Col: 62} | 765 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 118, Col: 62} |
| 676 | } | 766 | } | ||
| 677 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | 767 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) |
| 678 | if templ_7745c5c3_Err != nil { | 768 | if templ_7745c5c3_Err != nil { | ||
| 679 | return templ_7745c5c3_Err | 769 | return templ_7745c5c3_Err | ||
| 680 | } | 770 | } | ||
| 681 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</span> ") | 771 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</span> ") |
| 682 | if templ_7745c5c3_Err != nil { | 772 | if templ_7745c5c3_Err != nil { | ||
| 683 | return templ_7745c5c3_Err | 773 | return templ_7745c5c3_Err | ||
| 684 | } | 774 | } | ||
| 685 | if c.WhenAt != "" { | 775 | if c.WhenAt != "" { | ||
| 686 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<time class=\"dim\" datetime=\"") | 776 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<time class=\"dim\" datetime=\"") |
| 687 | if templ_7745c5c3_Err != nil { | 777 | if templ_7745c5c3_Err != nil { | ||
| 688 | return templ_7745c5c3_Err | 778 | return templ_7745c5c3_Err | ||
| 689 | } | 779 | } | ||
| 690 | - | var templ_7745c5c3_Var35 string | 780 | + | var templ_7745c5c3_Var41 string |
| 691 | - | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | 781 | + | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) |
| 692 | if templ_7745c5c3_Err != nil { | 782 | if templ_7745c5c3_Err != nil { | ||
| 693 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 114, Col: 44} | 783 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 120, Col: 44} |
| 694 | } | 784 | } | ||
| 695 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35) | 785 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var41) |
| 696 | if templ_7745c5c3_Err != nil { | 786 | if templ_7745c5c3_Err != nil { | ||
| 697 | return templ_7745c5c3_Err | 787 | return templ_7745c5c3_Err | ||
| 698 | } | 788 | } | ||
| 699 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" title=\"") | 789 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\" title=\"") |
| 700 | if templ_7745c5c3_Err != nil { | 790 | if templ_7745c5c3_Err != nil { | ||
| 701 | return templ_7745c5c3_Err | 791 | return templ_7745c5c3_Err | ||
| 702 | } | 792 | } | ||
| 703 | - | var templ_7745c5c3_Var36 string | 793 | + | var templ_7745c5c3_Var42 string |
| 704 | - | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | 794 | + | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) |
| 705 | if templ_7745c5c3_Err != nil { | 795 | if templ_7745c5c3_Err != nil { | ||
| 706 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 114, Col: 63} | 796 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 120, Col: 63} |
| 707 | } | 797 | } | ||
| 708 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36) | 798 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42) |
| 709 | if templ_7745c5c3_Err != nil { | 799 | if templ_7745c5c3_Err != nil { | ||
| 710 | return templ_7745c5c3_Err | 800 | return templ_7745c5c3_Err | ||
| 711 | } | 801 | } | ||
| 712 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\">") | 802 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "\">") |
| 713 | if templ_7745c5c3_Err != nil { | 803 | if templ_7745c5c3_Err != nil { | ||
| 714 | return templ_7745c5c3_Err | 804 | return templ_7745c5c3_Err | ||
| 715 | } | 805 | } | ||
| 716 | - | var templ_7745c5c3_Var37 string | 806 | + | var templ_7745c5c3_Var43 string |
| 717 | - | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | 807 | + | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) |
| 718 | if templ_7745c5c3_Err != nil { | 808 | if templ_7745c5c3_Err != nil { | ||
| 719 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 114, Col: 74} | 809 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 120, Col: 74} |
| 720 | } | 810 | } | ||
| 721 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) | 811 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) |
| 722 | if templ_7745c5c3_Err != nil { | 812 | if templ_7745c5c3_Err != nil { | ||
| 723 | return templ_7745c5c3_Err | 813 | return templ_7745c5c3_Err | ||
| 724 | } | 814 | } | ||
| 725 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</time>") | 815 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</time>") |
| 726 | if templ_7745c5c3_Err != nil { | 816 | if templ_7745c5c3_Err != nil { | ||
| 727 | return templ_7745c5c3_Err | 817 | return templ_7745c5c3_Err | ||
| 728 | } | 818 | } | ||
| 729 | } | 819 | } | ||
| 730 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</li>") | 820 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</li>") |
| 731 | if templ_7745c5c3_Err != nil { | 821 | if templ_7745c5c3_Err != nil { | ||
| 732 | return templ_7745c5c3_Err | 822 | return templ_7745c5c3_Err | ||
| 733 | } | 823 | } | ||
| 734 | } | 824 | } | ||
| 735 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</ul>") | 825 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</ul>") |
| 736 | if templ_7745c5c3_Err != nil { | 826 | if templ_7745c5c3_Err != nil { | ||
| 737 | return templ_7745c5c3_Err | 827 | return templ_7745c5c3_Err | ||
| 738 | } | 828 | } | ||
| 739 | } else if r.NoCommits { | 829 | } else if r.NoCommits { | ||
| 740 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<p class=\"repo-commits mute\">no commits yet</p>") | 830 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<p class=\"repo-commits mute\">no commits yet</p>") |
| 741 | if templ_7745c5c3_Err != nil { | 831 | if templ_7745c5c3_Err != nil { | ||
| 742 | return templ_7745c5c3_Err | 832 | return templ_7745c5c3_Err | ||
| 743 | } | 833 | } | ||
| 744 | } | 834 | } | ||
| 745 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</li>") | 835 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</li>") |
| 746 | if templ_7745c5c3_Err != nil { | 836 | if templ_7745c5c3_Err != nil { | ||
| 747 | return templ_7745c5c3_Err | 837 | return templ_7745c5c3_Err | ||
| 748 | } | 838 | } | ||
| @@ -770,113 +860,113 @@ func pulse(h *Heartbeat) templ.Component { | |||||
| 770 | }() | 860 | }() | ||
| 771 | } | 861 | } | ||
| 772 | ctx = templ.InitializeContext(ctx) | 862 | ctx = templ.InitializeContext(ctx) | ||
| 773 | - | templ_7745c5c3_Var38 := templ.GetChildren(ctx) | 863 | + | templ_7745c5c3_Var44 := templ.GetChildren(ctx) |
| 774 | - | if templ_7745c5c3_Var38 == nil { | 864 | + | if templ_7745c5c3_Var44 == nil { |
| 775 | - | templ_7745c5c3_Var38 = templ.NopComponent | 865 | + | templ_7745c5c3_Var44 = templ.NopComponent |
| 776 | } | 866 | } | ||
| 777 | ctx = templ.ClearChildren(ctx) | 867 | ctx = templ.ClearChildren(ctx) | ||
| 778 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<figure class=\"repo-pulse\"") | 868 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<figure class=\"repo-pulse\"") |
| 779 | if templ_7745c5c3_Err != nil { | 869 | if templ_7745c5c3_Err != nil { | ||
| 780 | return templ_7745c5c3_Err | 870 | return templ_7745c5c3_Err | ||
| 781 | } | 871 | } | ||
| 782 | if h.Live { | 872 | if h.Live { | ||
| 783 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, " data-live") | 873 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, " data-live") |
| 784 | if templ_7745c5c3_Err != nil { | 874 | if templ_7745c5c3_Err != nil { | ||
| 785 | return templ_7745c5c3_Err | 875 | return templ_7745c5c3_Err | ||
| 786 | } | 876 | } | ||
| 787 | } | 877 | } | ||
| 788 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "><svg viewBox=\"0 0 600 64\" role=\"img\" aria-label=\"") | 878 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "><svg viewBox=\"0 0 600 64\" role=\"img\" aria-label=\"") |
| 789 | if templ_7745c5c3_Err != nil { | 879 | if templ_7745c5c3_Err != nil { | ||
| 790 | return templ_7745c5c3_Err | 880 | return templ_7745c5c3_Err | ||
| 791 | } | 881 | } | ||
| 792 | - | var templ_7745c5c3_Var39 string | 882 | + | var templ_7745c5c3_Var45 string |
| 793 | - | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Label) | 883 | + | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Label) |
| 794 | if templ_7745c5c3_Err != nil { | 884 | if templ_7745c5c3_Err != nil { | ||
| 795 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 131, Col: 59} | 885 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 137, Col: 59} |
| 796 | } | 886 | } | ||
| 797 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39) | 887 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var45) |
| 798 | if templ_7745c5c3_Err != nil { | 888 | if templ_7745c5c3_Err != nil { | ||
| 799 | return templ_7745c5c3_Err | 889 | return templ_7745c5c3_Err | ||
| 800 | } | 890 | } | ||
| 801 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" focusable=\"false\"><defs><linearGradient id=\"") | 891 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" focusable=\"false\"><defs><linearGradient id=\"") |
| 802 | if templ_7745c5c3_Err != nil { | 892 | if templ_7745c5c3_Err != nil { | ||
| 803 | return templ_7745c5c3_Err | 893 | return templ_7745c5c3_Err | ||
| 804 | } | 894 | } | ||
| 805 | - | var templ_7745c5c3_Var40 string | 895 | + | var templ_7745c5c3_Var46 string |
| 806 | - | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.GradID) | 896 | + | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.GradID) |
| 807 | if templ_7745c5c3_Err != nil { | 897 | if templ_7745c5c3_Err != nil { | ||
| 808 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 133, Col: 33} | 898 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 139, Col: 33} |
| 809 | } | 899 | } | ||
| 810 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40) | 900 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var46) |
| 811 | if templ_7745c5c3_Err != nil { | 901 | if templ_7745c5c3_Err != nil { | ||
| 812 | return templ_7745c5c3_Err | 902 | return templ_7745c5c3_Err | ||
| 813 | } | 903 | } | ||
| 814 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop class=\"pulse-stop-top\" offset=\"0\"></stop> <stop class=\"pulse-stop-bottom\" offset=\"1\"></stop></linearGradient></defs> <path class=\"pulse-grid\" d=\"M0 21.5 H600 M0 42.5 H600 M0 63.5 H600\" vector-effect=\"non-scaling-stroke\"></path> <path class=\"pulse-area\" d=\"") | 904 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop class=\"pulse-stop-top\" offset=\"0\"></stop> <stop class=\"pulse-stop-bottom\" offset=\"1\"></stop></linearGradient></defs> <path class=\"pulse-grid\" d=\"M0 21.5 H600 M0 42.5 H600 M0 63.5 H600\" vector-effect=\"non-scaling-stroke\"></path> <path class=\"pulse-area\" d=\"") |
| 815 | if templ_7745c5c3_Err != nil { | 905 | if templ_7745c5c3_Err != nil { | ||
| 816 | return templ_7745c5c3_Err | 906 | return templ_7745c5c3_Err | ||
| 817 | } | 907 | } | ||
| 818 | - | var templ_7745c5c3_Var41 string | 908 | + | var templ_7745c5c3_Var47 string |
| 819 | - | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Area) | 909 | + | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Area) |
| 820 | if templ_7745c5c3_Err != nil { | 910 | if templ_7745c5c3_Err != nil { | ||
| 821 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 139, Col: 38} | 911 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 145, Col: 38} |
| 822 | } | 912 | } | ||
| 823 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var41) | 913 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47) |
| 824 | if templ_7745c5c3_Err != nil { | 914 | if templ_7745c5c3_Err != nil { | ||
| 825 | return templ_7745c5c3_Err | 915 | return templ_7745c5c3_Err | ||
| 826 | } | 916 | } | ||
| 827 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "\" fill=\"") | 917 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\" fill=\"") |
| 828 | if templ_7745c5c3_Err != nil { | 918 | if templ_7745c5c3_Err != nil { | ||
| 829 | return templ_7745c5c3_Err | 919 | return templ_7745c5c3_Err | ||
| 830 | } | 920 | } | ||
| 831 | - | var templ_7745c5c3_Var42 string | 921 | + | var templ_7745c5c3_Var48 string |
| 832 | - | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.ResolveAttributeValue("url(#" + h.GradID + ")") | 922 | + | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue("url(#" + h.GradID + ")") |
| 833 | if templ_7745c5c3_Err != nil { | 923 | if templ_7745c5c3_Err != nil { | ||
| 834 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 139, Col: 72} | 924 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 145, Col: 72} |
| 835 | } | 925 | } | ||
| 836 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42) | 926 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48) |
| 837 | if templ_7745c5c3_Err != nil { | 927 | if templ_7745c5c3_Err != nil { | ||
| 838 | return templ_7745c5c3_Err | 928 | return templ_7745c5c3_Err | ||
| 839 | } | 929 | } | ||
| 840 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\"></path> <path class=\"pulse-line\" d=\"") | 930 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "\"></path> <path class=\"pulse-line\" d=\"") |
| 841 | if templ_7745c5c3_Err != nil { | 931 | if templ_7745c5c3_Err != nil { | ||
| 842 | return templ_7745c5c3_Err | 932 | return templ_7745c5c3_Err | ||
| 843 | } | 933 | } | ||
| 844 | - | var templ_7745c5c3_Var43 string | 934 | + | var templ_7745c5c3_Var49 string |
| 845 | - | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Line) | 935 | + | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Line) |
| 846 | if templ_7745c5c3_Err != nil { | 936 | if templ_7745c5c3_Err != nil { | ||
| 847 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 140, Col: 38} | 937 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 146, Col: 38} |
| 848 | } | 938 | } | ||
| 849 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var43) | 939 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49) |
| 850 | if templ_7745c5c3_Err != nil { | 940 | if templ_7745c5c3_Err != nil { | ||
| 851 | return templ_7745c5c3_Err | 941 | return templ_7745c5c3_Err | ||
| 852 | } | 942 | } | ||
| 853 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\" vector-effect=\"non-scaling-stroke\"></path> <circle class=\"pulse-dot\" cx=\"") | 943 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "\" vector-effect=\"non-scaling-stroke\"></path> <circle class=\"pulse-dot\" cx=\"") |
| 854 | if templ_7745c5c3_Err != nil { | 944 | if templ_7745c5c3_Err != nil { | ||
| 855 | return templ_7745c5c3_Err | 945 | return templ_7745c5c3_Err | ||
| 856 | } | 946 | } | ||
| 857 | - | var templ_7745c5c3_Var44 string | 947 | + | var templ_7745c5c3_Var50 string |
| 858 | - | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.X) | 948 | + | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.X) |
| 859 | if templ_7745c5c3_Err != nil { | 949 | if templ_7745c5c3_Err != nil { | ||
| 860 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 141, Col: 37} | 950 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 147, Col: 37} |
| 861 | } | 951 | } | ||
| 862 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var44) | 952 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50) |
| 863 | if templ_7745c5c3_Err != nil { | 953 | if templ_7745c5c3_Err != nil { | ||
| 864 | return templ_7745c5c3_Err | 954 | return templ_7745c5c3_Err | ||
| 865 | } | 955 | } | ||
| 866 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\" cy=\"") | 956 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "\" cy=\"") |
| 867 | if templ_7745c5c3_Err != nil { | 957 | if templ_7745c5c3_Err != nil { | ||
| 868 | return templ_7745c5c3_Err | 958 | return templ_7745c5c3_Err | ||
| 869 | } | 959 | } | ||
| 870 | - | var templ_7745c5c3_Var45 string | 960 | + | var templ_7745c5c3_Var51 string |
| 871 | - | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Y) | 961 | + | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.ResolveAttributeValue(h.Y) |
| 872 | if templ_7745c5c3_Err != nil { | 962 | if templ_7745c5c3_Err != nil { | ||
| 873 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 141, Col: 48} | 963 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 147, Col: 48} |
| 874 | } | 964 | } | ||
| 875 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var45) | 965 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var51) |
| 876 | if templ_7745c5c3_Err != nil { | 966 | if templ_7745c5c3_Err != nil { | ||
| 877 | return templ_7745c5c3_Err | 967 | return templ_7745c5c3_Err | ||
| 878 | } | 968 | } | ||
| 879 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "\" r=\"3.5\"></circle></svg></figure>") | 969 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\" r=\"3.5\"></circle></svg></figure>") |
| 880 | if templ_7745c5c3_Err != nil { | 970 | if templ_7745c5c3_Err != nil { | ||
| 881 | return templ_7745c5c3_Err | 971 | return templ_7745c5c3_Err | ||
| 882 | } | 972 | } | ||
| @@ -901,66 +991,66 @@ func Outline(headings []Heading) templ.Component { | |||||
| 901 | }() | 991 | }() | ||
| 902 | } | 992 | } | ||
| 903 | ctx = templ.InitializeContext(ctx) | 993 | ctx = templ.InitializeContext(ctx) | ||
| 904 | - | templ_7745c5c3_Var46 := templ.GetChildren(ctx) | 994 | + | templ_7745c5c3_Var52 := templ.GetChildren(ctx) |
| 905 | - | if templ_7745c5c3_Var46 == nil { | 995 | + | if templ_7745c5c3_Var52 == nil { |
| 906 | - | templ_7745c5c3_Var46 = templ.NopComponent | 996 | + | templ_7745c5c3_Var52 = templ.NopComponent |
| 907 | } | 997 | } | ||
| 908 | ctx = templ.ClearChildren(ctx) | 998 | ctx = templ.ClearChildren(ctx) | ||
| 909 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<nav class=\"outline\" aria-label=\"Outline\">") | 999 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<nav class=\"outline\" aria-label=\"Outline\">") |
| 910 | if templ_7745c5c3_Err != nil { | 1000 | if templ_7745c5c3_Err != nil { | ||
| 911 | return templ_7745c5c3_Err | 1001 | return templ_7745c5c3_Err | ||
| 912 | } | 1002 | } | ||
| 913 | for _, h := range headings { | 1003 | for _, h := range headings { | ||
| 914 | - | var templ_7745c5c3_Var47 = []any{"l" + itoa(h.Level)} | 1004 | + | var templ_7745c5c3_Var53 = []any{"l" + itoa(h.Level)} |
| 915 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var47...) | 1005 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var53...) |
| 916 | if templ_7745c5c3_Err != nil { | 1006 | if templ_7745c5c3_Err != nil { | ||
| 917 | return templ_7745c5c3_Err | 1007 | return templ_7745c5c3_Err | ||
| 918 | } | 1008 | } | ||
| 919 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<a class=\"") | 1009 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<a class=\"") |
| 920 | if templ_7745c5c3_Err != nil { | 1010 | if templ_7745c5c3_Err != nil { | ||
| 921 | return templ_7745c5c3_Err | 1011 | return templ_7745c5c3_Err | ||
| 922 | } | 1012 | } | ||
| 923 | - | var templ_7745c5c3_Var48 string | 1013 | + | var templ_7745c5c3_Var54 string |
| 924 | - | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var47).String()) | 1014 | + | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var53).String()) |
| 925 | if templ_7745c5c3_Err != nil { | 1015 | if templ_7745c5c3_Err != nil { | ||
| 926 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 1, Col: 0} | 1016 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 1, Col: 0} | ||
| 927 | } | 1017 | } | ||
| 928 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48) | 1018 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var54) |
| 929 | if templ_7745c5c3_Err != nil { | 1019 | if templ_7745c5c3_Err != nil { | ||
| 930 | return templ_7745c5c3_Err | 1020 | return templ_7745c5c3_Err | ||
| 931 | } | 1021 | } | ||
| 932 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\" href=\"") | 1022 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\" href=\"") |
| 933 | if templ_7745c5c3_Err != nil { | 1023 | if templ_7745c5c3_Err != nil { | ||
| 934 | return templ_7745c5c3_Err | 1024 | return templ_7745c5c3_Err | ||
| 935 | } | 1025 | } | ||
| 936 | - | var templ_7745c5c3_Var49 templ.SafeURL | 1026 | + | var templ_7745c5c3_Var55 templ.SafeURL |
| 937 | - | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("#" + h.ID)) | 1027 | + | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("#" + h.ID)) |
| 938 | if templ_7745c5c3_Err != nil { | 1028 | if templ_7745c5c3_Err != nil { | ||
| 939 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 150, Col: 68} | 1029 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 156, Col: 68} |
| 940 | } | 1030 | } | ||
| 941 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) | 1031 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) |
| 942 | if templ_7745c5c3_Err != nil { | 1032 | if templ_7745c5c3_Err != nil { | ||
| 943 | return templ_7745c5c3_Err | 1033 | return templ_7745c5c3_Err | ||
| 944 | } | 1034 | } | ||
| 945 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\">") | 1035 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\">") |
| 946 | if templ_7745c5c3_Err != nil { | 1036 | if templ_7745c5c3_Err != nil { | ||
| 947 | return templ_7745c5c3_Err | 1037 | return templ_7745c5c3_Err | ||
| 948 | } | 1038 | } | ||
| 949 | - | var templ_7745c5c3_Var50 string | 1039 | + | var templ_7745c5c3_Var56 string |
| 950 | - | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(h.Text) | 1040 | + | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(h.Text) |
| 951 | if templ_7745c5c3_Err != nil { | 1041 | if templ_7745c5c3_Err != nil { | ||
| 952 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 150, Col: 79} | 1042 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 156, Col: 79} |
| 953 | } | 1043 | } | ||
| 954 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) | 1044 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) |
| 955 | if templ_7745c5c3_Err != nil { | 1045 | if templ_7745c5c3_Err != nil { | ||
| 956 | return templ_7745c5c3_Err | 1046 | return templ_7745c5c3_Err | ||
| 957 | } | 1047 | } | ||
| 958 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</a>") | 1048 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</a>") |
| 959 | if templ_7745c5c3_Err != nil { | 1049 | if templ_7745c5c3_Err != nil { | ||
| 960 | return templ_7745c5c3_Err | 1050 | return templ_7745c5c3_Err | ||
| 961 | } | 1051 | } | ||
| 962 | } | 1052 | } | ||
| 963 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</nav>") | 1053 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</nav>") |
| 964 | if templ_7745c5c3_Err != nil { | 1054 | if templ_7745c5c3_Err != nil { | ||
| 965 | return templ_7745c5c3_Err | 1055 | return templ_7745c5c3_Err | ||
| 966 | } | 1056 | } | ||
| @@ -969,7 +1059,7 @@ func Outline(headings []Heading) templ.Component { | |||||
| 969 | } | 1059 | } | ||
| 970 | 1060 | ||||
| 971 | // ReadmeCard is a README in a card whose header names the file. | 1061 | // ReadmeCard is a README in a card whose header names the file. | ||
| 972 | - | func ReadmeCard(r Readme) templ.Component { | 1062 | + | func ReadmeCard(r Readme, docs ...DocTab) templ.Component { |
| 973 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | 1063 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | ||
| 974 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | 1064 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | ||
| 975 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | 1065 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | ||
| @@ -985,33 +1075,103 @@ func ReadmeCard(r Readme) templ.Component { | |||||
| 985 | }() | 1075 | }() | ||
| 986 | } | 1076 | } | ||
| 987 | ctx = templ.InitializeContext(ctx) | 1077 | ctx = templ.InitializeContext(ctx) | ||
| 988 | - | templ_7745c5c3_Var51 := templ.GetChildren(ctx) | 1078 | + | templ_7745c5c3_Var57 := templ.GetChildren(ctx) |
| 989 | - | if templ_7745c5c3_Var51 == nil { | 1079 | + | if templ_7745c5c3_Var57 == nil { |
| 990 | - | templ_7745c5c3_Var51 = templ.NopComponent | 1080 | + | templ_7745c5c3_Var57 = templ.NopComponent |
| 991 | } | 1081 | } | ||
| 992 | ctx = templ.ClearChildren(ctx) | 1082 | ctx = templ.ClearChildren(ctx) | ||
| 993 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<section class=\"card readme\" aria-labelledby=\"readme-h\"><h2 class=\"card-h\" id=\"readme-h\"><span class=\"ico i-file\" aria-hidden=\"true\"></span> ") | 1083 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "<section class=\"card readme\" aria-label=\"") |
| 994 | if templ_7745c5c3_Err != nil { | 1084 | if templ_7745c5c3_Err != nil { | ||
| 995 | return templ_7745c5c3_Err | 1085 | return templ_7745c5c3_Err | ||
| 996 | } | 1086 | } | ||
| 997 | - | var templ_7745c5c3_Var52 string | 1087 | + | var templ_7745c5c3_Var58 string |
| 998 | - | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | 1088 | + | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name) |
| 999 | if templ_7745c5c3_Err != nil { | 1089 | if templ_7745c5c3_Err != nil { | ||
| 1000 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 160, Col: 11} | 1090 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 163, Col: 49} |
| 1001 | } | 1091 | } | ||
| 1002 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) | 1092 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var58) |
| 1003 | if templ_7745c5c3_Err != nil { | 1093 | if templ_7745c5c3_Err != nil { | ||
| 1004 | return templ_7745c5c3_Err | 1094 | return templ_7745c5c3_Err | ||
| 1005 | } | 1095 | } | ||
| 1006 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</h2>") | 1096 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "\">") |
| 1007 | if templ_7745c5c3_Err != nil { | 1097 | if templ_7745c5c3_Err != nil { | ||
| 1008 | return templ_7745c5c3_Err | 1098 | return templ_7745c5c3_Err | ||
| 1009 | } | 1099 | } | ||
| 1100 | + | if len(docs) > 1 { | |||
| 1101 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "<nav class=\"card-h doc-tabs\" aria-label=\"Repository documents\">") | |||
| 1102 | + | if templ_7745c5c3_Err != nil { | |||
| 1103 | + | return templ_7745c5c3_Err | |||
| 1104 | + | } | |||
| 1105 | + | for _, doc := range docs { | |||
| 1106 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<a href=\"") | |||
| 1107 | + | if templ_7745c5c3_Err != nil { | |||
| 1108 | + | return templ_7745c5c3_Err | |||
| 1109 | + | } | |||
| 1110 | + | var templ_7745c5c3_Var59 templ.SafeURL | |||
| 1111 | + | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(doc.Href)) | |||
| 1112 | + | if templ_7745c5c3_Err != nil { | |||
| 1113 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 167, Col: 38} | |||
| 1114 | + | } | |||
| 1115 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) | |||
| 1116 | + | if templ_7745c5c3_Err != nil { | |||
| 1117 | + | return templ_7745c5c3_Err | |||
| 1118 | + | } | |||
| 1119 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "\"") | |||
| 1120 | + | if templ_7745c5c3_Err != nil { | |||
| 1121 | + | return templ_7745c5c3_Err | |||
| 1122 | + | } | |||
| 1123 | + | if doc.Current { | |||
| 1124 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, " aria-current=\"page\"") | |||
| 1125 | + | if templ_7745c5c3_Err != nil { | |||
| 1126 | + | return templ_7745c5c3_Err | |||
| 1127 | + | } | |||
| 1128 | + | } | |||
| 1129 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, ">") | |||
| 1130 | + | if templ_7745c5c3_Err != nil { | |||
| 1131 | + | return templ_7745c5c3_Err | |||
| 1132 | + | } | |||
| 1133 | + | var templ_7745c5c3_Var60 string | |||
| 1134 | + | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Name) | |||
| 1135 | + | if templ_7745c5c3_Err != nil { | |||
| 1136 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 171, Col: 16} | |||
| 1137 | + | } | |||
| 1138 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) | |||
| 1139 | + | if templ_7745c5c3_Err != nil { | |||
| 1140 | + | return templ_7745c5c3_Err | |||
| 1141 | + | } | |||
| 1142 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</a>") | |||
| 1143 | + | if templ_7745c5c3_Err != nil { | |||
| 1144 | + | return templ_7745c5c3_Err | |||
| 1145 | + | } | |||
| 1146 | + | } | |||
| 1147 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</nav>") | |||
| 1148 | + | if templ_7745c5c3_Err != nil { | |||
| 1149 | + | return templ_7745c5c3_Err | |||
| 1150 | + | } | |||
| 1151 | + | } else { | |||
| 1152 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<h2 class=\"card-h\" id=\"readme-h\"><span class=\"ico i-file\" aria-hidden=\"true\"></span> ") | |||
| 1153 | + | if templ_7745c5c3_Err != nil { | |||
| 1154 | + | return templ_7745c5c3_Err | |||
| 1155 | + | } | |||
| 1156 | + | var templ_7745c5c3_Var61 string | |||
| 1157 | + | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name) | |||
| 1158 | + | if templ_7745c5c3_Err != nil { | |||
| 1159 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 177, Col: 12} | |||
| 1160 | + | } | |||
| 1161 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | |||
| 1162 | + | if templ_7745c5c3_Err != nil { | |||
| 1163 | + | return templ_7745c5c3_Err | |||
| 1164 | + | } | |||
| 1165 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</h2>") | |||
| 1166 | + | if templ_7745c5c3_Err != nil { | |||
| 1167 | + | return templ_7745c5c3_Err | |||
| 1168 | + | } | |||
| 1169 | + | } | |||
| 1010 | templ_7745c5c3_Err = ReadmeView(r).Render(ctx, templ_7745c5c3_Buffer) | 1170 | templ_7745c5c3_Err = ReadmeView(r).Render(ctx, templ_7745c5c3_Buffer) | ||
| 1011 | if templ_7745c5c3_Err != nil { | 1171 | if templ_7745c5c3_Err != nil { | ||
| 1012 | return templ_7745c5c3_Err | 1172 | return templ_7745c5c3_Err | ||
| 1013 | } | 1173 | } | ||
| 1014 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "</section>") | 1174 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</section>") |
| 1015 | if templ_7745c5c3_Err != nil { | 1175 | if templ_7745c5c3_Err != nil { | ||
| 1016 | return templ_7745c5c3_Err | 1176 | return templ_7745c5c3_Err | ||
| 1017 | } | 1177 | } | ||
| @@ -1037,43 +1197,43 @@ func ReadmeView(r Readme) templ.Component { | |||||
| 1037 | }() | 1197 | }() | ||
| 1038 | } | 1198 | } | ||
| 1039 | ctx = templ.InitializeContext(ctx) | 1199 | ctx = templ.InitializeContext(ctx) | ||
| 1040 | - | templ_7745c5c3_Var53 := templ.GetChildren(ctx) | 1200 | + | templ_7745c5c3_Var62 := templ.GetChildren(ctx) |
| 1041 | - | if templ_7745c5c3_Var53 == nil { | 1201 | + | if templ_7745c5c3_Var62 == nil { |
| 1042 | - | templ_7745c5c3_Var53 = templ.NopComponent | 1202 | + | templ_7745c5c3_Var62 = templ.NopComponent |
| 1043 | } | 1203 | } | ||
| 1044 | ctx = templ.ClearChildren(ctx) | 1204 | ctx = templ.ClearChildren(ctx) | ||
| 1045 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<article class=\"prose\" aria-label=\"") | 1205 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<article class=\"prose\" aria-label=\"") |
| 1046 | if templ_7745c5c3_Err != nil { | 1206 | if templ_7745c5c3_Err != nil { | ||
| 1047 | return templ_7745c5c3_Err | 1207 | return templ_7745c5c3_Err | ||
| 1048 | } | 1208 | } | ||
| 1049 | - | var templ_7745c5c3_Var54 string | 1209 | + | var templ_7745c5c3_Var63 string |
| 1050 | - | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name) | 1210 | + | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name) |
| 1051 | if templ_7745c5c3_Err != nil { | 1211 | if templ_7745c5c3_Err != nil { | ||
| 1052 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 169, Col: 43} | 1212 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 187, Col: 43} |
| 1053 | } | 1213 | } | ||
| 1054 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var54) | 1214 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63) |
| 1055 | if templ_7745c5c3_Err != nil { | 1215 | if templ_7745c5c3_Err != nil { | ||
| 1056 | return templ_7745c5c3_Err | 1216 | return templ_7745c5c3_Err | ||
| 1057 | } | 1217 | } | ||
| 1058 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\" hx-ignore>") | 1218 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\" hx-ignore>") |
| 1059 | if templ_7745c5c3_Err != nil { | 1219 | if templ_7745c5c3_Err != nil { | ||
| 1060 | return templ_7745c5c3_Err | 1220 | return templ_7745c5c3_Err | ||
| 1061 | } | 1221 | } | ||
| 1062 | if r.Note != "" { | 1222 | if r.Note != "" { | ||
| 1063 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "<p class=\"dim\">") | 1223 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<p class=\"dim\">") |
| 1064 | if templ_7745c5c3_Err != nil { | 1224 | if templ_7745c5c3_Err != nil { | ||
| 1065 | return templ_7745c5c3_Err | 1225 | return templ_7745c5c3_Err | ||
| 1066 | } | 1226 | } | ||
| 1067 | - | var templ_7745c5c3_Var55 string | 1227 | + | var templ_7745c5c3_Var64 string |
| 1068 | - | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(r.Note) | 1228 | + | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(r.Note) |
| 1069 | if templ_7745c5c3_Err != nil { | 1229 | if templ_7745c5c3_Err != nil { | ||
| 1070 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 171, Col: 26} | 1230 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 189, Col: 26} |
| 1071 | } | 1231 | } | ||
| 1072 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | 1232 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64)) |
| 1073 | if templ_7745c5c3_Err != nil { | 1233 | if templ_7745c5c3_Err != nil { | ||
| 1074 | return templ_7745c5c3_Err | 1234 | return templ_7745c5c3_Err | ||
| 1075 | } | 1235 | } | ||
| 1076 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</p>") | 1236 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</p>") |
| 1077 | if templ_7745c5c3_Err != nil { | 1237 | if templ_7745c5c3_Err != nil { | ||
| 1078 | return templ_7745c5c3_Err | 1238 | return templ_7745c5c3_Err | ||
| 1079 | } | 1239 | } | ||
| @@ -1083,7 +1243,7 @@ func ReadmeView(r Readme) templ.Component { | |||||
| 1083 | return templ_7745c5c3_Err | 1243 | return templ_7745c5c3_Err | ||
| 1084 | } | 1244 | } | ||
| 1085 | } | 1245 | } | ||
| 1086 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</article>") | 1246 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</article>") |
| 1087 | if templ_7745c5c3_Err != nil { | 1247 | if templ_7745c5c3_Err != nil { | ||
| 1088 | return templ_7745c5c3_Err | 1248 | return templ_7745c5c3_Err | ||
| 1089 | } | 1249 | } | ||
| @@ -1109,90 +1269,90 @@ func CommitRows(rows []CommitRow) templ.Component { | |||||
| 1109 | }() | 1269 | }() | ||
| 1110 | } | 1270 | } | ||
| 1111 | ctx = templ.InitializeContext(ctx) | 1271 | ctx = templ.InitializeContext(ctx) | ||
| 1112 | - | templ_7745c5c3_Var56 := templ.GetChildren(ctx) | 1272 | + | templ_7745c5c3_Var65 := templ.GetChildren(ctx) |
| 1113 | - | if templ_7745c5c3_Var56 == nil { | 1273 | + | if templ_7745c5c3_Var65 == nil { |
| 1114 | - | templ_7745c5c3_Var56 = templ.NopComponent | 1274 | + | templ_7745c5c3_Var65 = templ.NopComponent |
| 1115 | } | 1275 | } | ||
| 1116 | ctx = templ.ClearChildren(ctx) | 1276 | ctx = templ.ClearChildren(ctx) | ||
| 1117 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "<table class=\"list commits\" aria-label=\"Commits\"><tbody data-rows>") | 1277 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "<table class=\"list commits\" aria-label=\"Commits\"><tbody data-rows>") |
| 1118 | if templ_7745c5c3_Err != nil { | 1278 | if templ_7745c5c3_Err != nil { | ||
| 1119 | return templ_7745c5c3_Err | 1279 | return templ_7745c5c3_Err | ||
| 1120 | } | 1280 | } | ||
| 1121 | for _, c := range rows { | 1281 | for _, c := range rows { | ||
| 1122 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<tr") | 1282 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<tr") |
| 1123 | if templ_7745c5c3_Err != nil { | 1283 | if templ_7745c5c3_Err != nil { | ||
| 1124 | return templ_7745c5c3_Err | 1284 | return templ_7745c5c3_Err | ||
| 1125 | } | 1285 | } | ||
| 1126 | if c.ID != "" { | 1286 | if c.ID != "" { | ||
| 1127 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " id=\"") | 1287 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, " id=\"") |
| 1128 | if templ_7745c5c3_Err != nil { | 1288 | if templ_7745c5c3_Err != nil { | ||
| 1129 | return templ_7745c5c3_Err | 1289 | return templ_7745c5c3_Err | ||
| 1130 | } | 1290 | } | ||
| 1131 | - | var templ_7745c5c3_Var57 string | 1291 | + | var templ_7745c5c3_Var66 string |
| 1132 | - | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.ID) | 1292 | + | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.ID) |
| 1133 | if templ_7745c5c3_Err != nil { | 1293 | if templ_7745c5c3_Err != nil { | ||
| 1134 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 186, Col: 15} | 1294 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 204, Col: 15} |
| 1135 | } | 1295 | } | ||
| 1136 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var57) | 1296 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66) |
| 1137 | if templ_7745c5c3_Err != nil { | 1297 | if templ_7745c5c3_Err != nil { | ||
| 1138 | return templ_7745c5c3_Err | 1298 | return templ_7745c5c3_Err | ||
| 1139 | } | 1299 | } | ||
| 1140 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"") | 1300 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\"") |
| 1141 | if templ_7745c5c3_Err != nil { | 1301 | if templ_7745c5c3_Err != nil { | ||
| 1142 | return templ_7745c5c3_Err | 1302 | return templ_7745c5c3_Err | ||
| 1143 | } | 1303 | } | ||
| 1144 | } | 1304 | } | ||
| 1145 | if c.Cursor { | 1305 | if c.Cursor { | ||
| 1146 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " data-cursor") | 1306 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, " data-cursor") |
| 1147 | if templ_7745c5c3_Err != nil { | 1307 | if templ_7745c5c3_Err != nil { | ||
| 1148 | return templ_7745c5c3_Err | 1308 | return templ_7745c5c3_Err | ||
| 1149 | } | 1309 | } | ||
| 1150 | } | 1310 | } | ||
| 1151 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "><td class=\"c-sha mute\">") | 1311 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "><td class=\"c-sha mute\">") |
| 1152 | if templ_7745c5c3_Err != nil { | 1312 | if templ_7745c5c3_Err != nil { | ||
| 1153 | return templ_7745c5c3_Err | 1313 | return templ_7745c5c3_Err | ||
| 1154 | } | 1314 | } | ||
| 1155 | - | var templ_7745c5c3_Var58 string | 1315 | + | var templ_7745c5c3_Var67 string |
| 1156 | - | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | 1316 | + | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) |
| 1157 | if templ_7745c5c3_Err != nil { | 1317 | if templ_7745c5c3_Err != nil { | ||
| 1158 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 190, Col: 37} | 1318 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 208, Col: 37} |
| 1159 | } | 1319 | } | ||
| 1160 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) | 1320 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67)) |
| 1161 | if templ_7745c5c3_Err != nil { | 1321 | if templ_7745c5c3_Err != nil { | ||
| 1162 | return templ_7745c5c3_Err | 1322 | return templ_7745c5c3_Err | ||
| 1163 | } | 1323 | } | ||
| 1164 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "</td><td class=\"c-subject\"><a href=\"") | 1324 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "</td><td class=\"c-subject\"><a href=\"") |
| 1165 | if templ_7745c5c3_Err != nil { | 1325 | if templ_7745c5c3_Err != nil { | ||
| 1166 | return templ_7745c5c3_Err | 1326 | return templ_7745c5c3_Err | ||
| 1167 | } | 1327 | } | ||
| 1168 | - | var templ_7745c5c3_Var59 templ.SafeURL | 1328 | + | var templ_7745c5c3_Var68 templ.SafeURL |
| 1169 | - | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | 1329 | + | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) |
| 1170 | if templ_7745c5c3_Err != nil { | 1330 | if templ_7745c5c3_Err != nil { | ||
| 1171 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 192, Col: 38} | 1331 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 210, Col: 38} |
| 1172 | } | 1332 | } | ||
| 1173 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) | 1333 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) |
| 1174 | if templ_7745c5c3_Err != nil { | 1334 | if templ_7745c5c3_Err != nil { | ||
| 1175 | return templ_7745c5c3_Err | 1335 | return templ_7745c5c3_Err | ||
| 1176 | } | 1336 | } | ||
| 1177 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "\">") | 1337 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\">") |
| 1178 | if templ_7745c5c3_Err != nil { | 1338 | if templ_7745c5c3_Err != nil { | ||
| 1179 | return templ_7745c5c3_Err | 1339 | return templ_7745c5c3_Err | ||
| 1180 | } | 1340 | } | ||
| 1181 | - | var templ_7745c5c3_Var60 string | 1341 | + | var templ_7745c5c3_Var69 string |
| 1182 | - | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | 1342 | + | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) |
| 1183 | if templ_7745c5c3_Err != nil { | 1343 | if templ_7745c5c3_Err != nil { | ||
| 1184 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 192, Col: 52} | 1344 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 210, Col: 52} |
| 1185 | } | 1345 | } | ||
| 1186 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) | 1346 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) |
| 1187 | if templ_7745c5c3_Err != nil { | 1347 | if templ_7745c5c3_Err != nil { | ||
| 1188 | return templ_7745c5c3_Err | 1348 | return templ_7745c5c3_Err | ||
| 1189 | } | 1349 | } | ||
| 1190 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</a> ") | 1350 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</a> ") |
| 1191 | if templ_7745c5c3_Err != nil { | 1351 | if templ_7745c5c3_Err != nil { | ||
| 1192 | return templ_7745c5c3_Err | 1352 | return templ_7745c5c3_Err | ||
| 1193 | } | 1353 | } | ||
| 1194 | if c.Merge { | 1354 | if c.Merge { | ||
| 1195 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<span class=\"pill\">merge</span> ") | 1355 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<span class=\"pill\">merge</span> ") |
| 1196 | if templ_7745c5c3_Err != nil { | 1356 | if templ_7745c5c3_Err != nil { | ||
| 1197 | return templ_7745c5c3_Err | 1357 | return templ_7745c5c3_Err | ||
| 1198 | } | 1358 | } | ||
| @@ -1204,44 +1364,44 @@ func CommitRows(rows []CommitRow) templ.Component { | |||||
| 1204 | } | 1364 | } | ||
| 1205 | } | 1365 | } | ||
| 1206 | for _, b := range c.Branches { | 1366 | for _, b := range c.Branches { | ||
| 1207 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "<span class=\"pill\">") | 1367 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<span class=\"pill\">") |
| 1208 | if templ_7745c5c3_Err != nil { | 1368 | if templ_7745c5c3_Err != nil { | ||
| 1209 | return templ_7745c5c3_Err | 1369 | return templ_7745c5c3_Err | ||
| 1210 | } | 1370 | } | ||
| 1211 | - | var templ_7745c5c3_Var61 string | 1371 | + | var templ_7745c5c3_Var70 string |
| 1212 | - | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(b) | 1372 | + | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(b) |
| 1213 | if templ_7745c5c3_Err != nil { | 1373 | if templ_7745c5c3_Err != nil { | ||
| 1214 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 200, Col: 29} | 1374 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 218, Col: 29} |
| 1215 | } | 1375 | } | ||
| 1216 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | 1376 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) |
| 1217 | if templ_7745c5c3_Err != nil { | 1377 | if templ_7745c5c3_Err != nil { | ||
| 1218 | return templ_7745c5c3_Err | 1378 | return templ_7745c5c3_Err | ||
| 1219 | } | 1379 | } | ||
| 1220 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</span> ") | 1380 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</span> ") |
| 1221 | if templ_7745c5c3_Err != nil { | 1381 | if templ_7745c5c3_Err != nil { | ||
| 1222 | return templ_7745c5c3_Err | 1382 | return templ_7745c5c3_Err | ||
| 1223 | } | 1383 | } | ||
| 1224 | } | 1384 | } | ||
| 1225 | for _, t := range c.Tags { | 1385 | for _, t := range c.Tags { | ||
| 1226 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<span class=\"pill tag\"><span class=\"sr\">tag </span>") | 1386 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "<span class=\"pill tag\"><span class=\"sr\">tag </span>") |
| 1227 | if templ_7745c5c3_Err != nil { | 1387 | if templ_7745c5c3_Err != nil { | ||
| 1228 | return templ_7745c5c3_Err | 1388 | return templ_7745c5c3_Err | ||
| 1229 | } | 1389 | } | ||
| 1230 | - | var templ_7745c5c3_Var62 string | 1390 | + | var templ_7745c5c3_Var71 string |
| 1231 | - | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(t) | 1391 | + | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(t) |
| 1232 | if templ_7745c5c3_Err != nil { | 1392 | if templ_7745c5c3_Err != nil { | ||
| 1233 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 203, Col: 61} | 1393 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 221, Col: 61} |
| 1234 | } | 1394 | } | ||
| 1235 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | 1395 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) |
| 1236 | if templ_7745c5c3_Err != nil { | 1396 | if templ_7745c5c3_Err != nil { | ||
| 1237 | return templ_7745c5c3_Err | 1397 | return templ_7745c5c3_Err | ||
| 1238 | } | 1398 | } | ||
| 1239 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</span>") | 1399 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "</span>") |
| 1240 | if templ_7745c5c3_Err != nil { | 1400 | if templ_7745c5c3_Err != nil { | ||
| 1241 | return templ_7745c5c3_Err | 1401 | return templ_7745c5c3_Err | ||
| 1242 | } | 1402 | } | ||
| 1243 | } | 1403 | } | ||
| 1244 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "</td><td class=\"c-ck\">") | 1404 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "</td><td class=\"c-ck\">") |
| 1245 | if templ_7745c5c3_Err != nil { | 1405 | if templ_7745c5c3_Err != nil { | ||
| 1246 | return templ_7745c5c3_Err | 1406 | return templ_7745c5c3_Err | ||
| 1247 | } | 1407 | } | ||
| @@ -1249,64 +1409,64 @@ func CommitRows(rows []CommitRow) templ.Component { | |||||
| 1249 | if templ_7745c5c3_Err != nil { | 1409 | if templ_7745c5c3_Err != nil { | ||
| 1250 | return templ_7745c5c3_Err | 1410 | return templ_7745c5c3_Err | ||
| 1251 | } | 1411 | } | ||
| 1252 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</td><td class=\"c-who dim\">") | 1412 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "</td><td class=\"c-who dim\">") |
| 1253 | if templ_7745c5c3_Err != nil { | 1413 | if templ_7745c5c3_Err != nil { | ||
| 1254 | return templ_7745c5c3_Err | 1414 | return templ_7745c5c3_Err | ||
| 1255 | } | 1415 | } | ||
| 1256 | - | var templ_7745c5c3_Var63 string | 1416 | + | var templ_7745c5c3_Var72 string |
| 1257 | - | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | 1417 | + | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) |
| 1258 | if templ_7745c5c3_Err != nil { | 1418 | if templ_7745c5c3_Err != nil { | ||
| 1259 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 207, Col: 37} | 1419 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 225, Col: 37} |
| 1260 | } | 1420 | } | ||
| 1261 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) | 1421 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) |
| 1262 | if templ_7745c5c3_Err != nil { | 1422 | if templ_7745c5c3_Err != nil { | ||
| 1263 | return templ_7745c5c3_Err | 1423 | return templ_7745c5c3_Err | ||
| 1264 | } | 1424 | } | ||
| 1265 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</td><td class=\"c-when nowrap dim\"><time datetime=\"") | 1425 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</td><td class=\"c-when nowrap dim\"><time datetime=\"") |
| 1266 | if templ_7745c5c3_Err != nil { | 1426 | if templ_7745c5c3_Err != nil { | ||
| 1267 | return templ_7745c5c3_Err | 1427 | return templ_7745c5c3_Err | ||
| 1268 | } | 1428 | } | ||
| 1269 | - | var templ_7745c5c3_Var64 string | 1429 | + | var templ_7745c5c3_Var73 string |
| 1270 | - | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | 1430 | + | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) |
| 1271 | if templ_7745c5c3_Err != nil { | 1431 | if templ_7745c5c3_Err != nil { | ||
| 1272 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 208, Col: 60} | 1432 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 226, Col: 60} |
| 1273 | } | 1433 | } | ||
| 1274 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64) | 1434 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var73) |
| 1275 | if templ_7745c5c3_Err != nil { | 1435 | if templ_7745c5c3_Err != nil { | ||
| 1276 | return templ_7745c5c3_Err | 1436 | return templ_7745c5c3_Err | ||
| 1277 | } | 1437 | } | ||
| 1278 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\" title=\"") | 1438 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "\" title=\"") |
| 1279 | if templ_7745c5c3_Err != nil { | 1439 | if templ_7745c5c3_Err != nil { | ||
| 1280 | return templ_7745c5c3_Err | 1440 | return templ_7745c5c3_Err | ||
| 1281 | } | 1441 | } | ||
| 1282 | - | var templ_7745c5c3_Var65 string | 1442 | + | var templ_7745c5c3_Var74 string |
| 1283 | - | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | 1443 | + | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) |
| 1284 | if templ_7745c5c3_Err != nil { | 1444 | if templ_7745c5c3_Err != nil { | ||
| 1285 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 208, Col: 79} | 1445 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 226, Col: 79} |
| 1286 | } | 1446 | } | ||
| 1287 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var65) | 1447 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74) |
| 1288 | if templ_7745c5c3_Err != nil { | 1448 | if templ_7745c5c3_Err != nil { | ||
| 1289 | return templ_7745c5c3_Err | 1449 | return templ_7745c5c3_Err | ||
| 1290 | } | 1450 | } | ||
| 1291 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "\">") | 1451 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "\">") |
| 1292 | if templ_7745c5c3_Err != nil { | 1452 | if templ_7745c5c3_Err != nil { | ||
| 1293 | return templ_7745c5c3_Err | 1453 | return templ_7745c5c3_Err | ||
| 1294 | } | 1454 | } | ||
| 1295 | - | var templ_7745c5c3_Var66 string | 1455 | + | var templ_7745c5c3_Var75 string |
| 1296 | - | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | 1456 | + | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) |
| 1297 | if templ_7745c5c3_Err != nil { | 1457 | if templ_7745c5c3_Err != nil { | ||
| 1298 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 208, Col: 90} | 1458 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 226, Col: 90} |
| 1299 | } | 1459 | } | ||
| 1300 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) | 1460 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) |
| 1301 | if templ_7745c5c3_Err != nil { | 1461 | if templ_7745c5c3_Err != nil { | ||
| 1302 | return templ_7745c5c3_Err | 1462 | return templ_7745c5c3_Err | ||
| 1303 | } | 1463 | } | ||
| 1304 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</time></td></tr>") | 1464 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "</time></td></tr>") |
| 1305 | if templ_7745c5c3_Err != nil { | 1465 | if templ_7745c5c3_Err != nil { | ||
| 1306 | return templ_7745c5c3_Err | 1466 | return templ_7745c5c3_Err | ||
| 1307 | } | 1467 | } | ||
| 1308 | } | 1468 | } | ||
| 1309 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</tbody></table>") | 1469 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "</tbody></table>") |
| 1310 | if templ_7745c5c3_Err != nil { | 1470 | if templ_7745c5c3_Err != nil { | ||
| 1311 | return templ_7745c5c3_Err | 1471 | return templ_7745c5c3_Err | ||
| 1312 | } | 1472 | } | ||
| @@ -1332,75 +1492,75 @@ func signaturePill(b *SignatureBadge) templ.Component { | |||||
| 1332 | }() | 1492 | }() | ||
| 1333 | } | 1493 | } | ||
| 1334 | ctx = templ.InitializeContext(ctx) | 1494 | ctx = templ.InitializeContext(ctx) | ||
| 1335 | - | templ_7745c5c3_Var67 := templ.GetChildren(ctx) | 1495 | + | templ_7745c5c3_Var76 := templ.GetChildren(ctx) |
| 1336 | - | if templ_7745c5c3_Var67 == nil { | 1496 | + | if templ_7745c5c3_Var76 == nil { |
| 1337 | - | templ_7745c5c3_Var67 = templ.NopComponent | 1497 | + | templ_7745c5c3_Var76 = templ.NopComponent |
| 1338 | } | 1498 | } | ||
| 1339 | ctx = templ.ClearChildren(ctx) | 1499 | ctx = templ.ClearChildren(ctx) | ||
| 1340 | - | var templ_7745c5c3_Var68 = []any{"pill sig", b.Class} | 1500 | + | var templ_7745c5c3_Var77 = []any{"pill sig", b.Class} |
| 1341 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var68...) | 1501 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var77...) |
| 1342 | if templ_7745c5c3_Err != nil { | 1502 | if templ_7745c5c3_Err != nil { | ||
| 1343 | return templ_7745c5c3_Err | 1503 | return templ_7745c5c3_Err | ||
| 1344 | } | 1504 | } | ||
| 1345 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "<span class=\"") | 1505 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "<span class=\"") |
| 1346 | if templ_7745c5c3_Err != nil { | 1506 | if templ_7745c5c3_Err != nil { | ||
| 1347 | return templ_7745c5c3_Err | 1507 | return templ_7745c5c3_Err | ||
| 1348 | } | 1508 | } | ||
| 1349 | - | var templ_7745c5c3_Var69 string | 1509 | + | var templ_7745c5c3_Var78 string |
| 1350 | - | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var68).String()) | 1510 | + | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var77).String()) |
| 1351 | if templ_7745c5c3_Err != nil { | 1511 | if templ_7745c5c3_Err != nil { | ||
| 1352 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 1, Col: 0} | 1512 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 1, Col: 0} | ||
| 1353 | } | 1513 | } | ||
| 1354 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69) | 1514 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var78) |
| 1355 | if templ_7745c5c3_Err != nil { | 1515 | if templ_7745c5c3_Err != nil { | ||
| 1356 | return templ_7745c5c3_Err | 1516 | return templ_7745c5c3_Err | ||
| 1357 | } | 1517 | } | ||
| 1358 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "\" title=\"") | 1518 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "\" title=\"") |
| 1359 | if templ_7745c5c3_Err != nil { | 1519 | if templ_7745c5c3_Err != nil { | ||
| 1360 | return templ_7745c5c3_Err | 1520 | return templ_7745c5c3_Err | ||
| 1361 | } | 1521 | } | ||
| 1362 | - | var templ_7745c5c3_Var70 string | 1522 | + | var templ_7745c5c3_Var79 string |
| 1363 | - | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.ResolveAttributeValue(b.Title) | 1523 | + | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(b.Title) |
| 1364 | if templ_7745c5c3_Err != nil { | 1524 | if templ_7745c5c3_Err != nil { | ||
| 1365 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 218, Col: 52} | 1525 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 236, Col: 52} |
| 1366 | } | 1526 | } | ||
| 1367 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var70) | 1527 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79) |
| 1368 | if templ_7745c5c3_Err != nil { | 1528 | if templ_7745c5c3_Err != nil { | ||
| 1369 | return templ_7745c5c3_Err | 1529 | return templ_7745c5c3_Err | ||
| 1370 | } | 1530 | } | ||
| 1371 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\">") | 1531 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "\">") |
| 1372 | if templ_7745c5c3_Err != nil { | 1532 | if templ_7745c5c3_Err != nil { | ||
| 1373 | return templ_7745c5c3_Err | 1533 | return templ_7745c5c3_Err | ||
| 1374 | } | 1534 | } | ||
| 1375 | if b.Proto != "" { | 1535 | if b.Proto != "" { | ||
| 1376 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<span class=\"sig-proto\">") | 1536 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<span class=\"sig-proto\">") |
| 1377 | if templ_7745c5c3_Err != nil { | 1537 | if templ_7745c5c3_Err != nil { | ||
| 1378 | return templ_7745c5c3_Err | 1538 | return templ_7745c5c3_Err | ||
| 1379 | } | 1539 | } | ||
| 1380 | - | var templ_7745c5c3_Var71 string | 1540 | + | var templ_7745c5c3_Var80 string |
| 1381 | - | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(b.Proto) | 1541 | + | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.JoinStringErrs(b.Proto) |
| 1382 | if templ_7745c5c3_Err != nil { | 1542 | if templ_7745c5c3_Err != nil { | ||
| 1383 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 220, Col: 36} | 1543 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 238, Col: 36} |
| 1384 | } | 1544 | } | ||
| 1385 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | 1545 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var80)) |
| 1386 | if templ_7745c5c3_Err != nil { | 1546 | if templ_7745c5c3_Err != nil { | ||
| 1387 | return templ_7745c5c3_Err | 1547 | return templ_7745c5c3_Err | ||
| 1388 | } | 1548 | } | ||
| 1389 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</span> ") | 1549 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "</span> ") |
| 1390 | if templ_7745c5c3_Err != nil { | 1550 | if templ_7745c5c3_Err != nil { | ||
| 1391 | return templ_7745c5c3_Err | 1551 | return templ_7745c5c3_Err | ||
| 1392 | } | 1552 | } | ||
| 1393 | } | 1553 | } | ||
| 1394 | - | var templ_7745c5c3_Var72 string | 1554 | + | var templ_7745c5c3_Var81 string |
| 1395 | - | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label) | 1555 | + | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label) |
| 1396 | if templ_7745c5c3_Err != nil { | 1556 | if templ_7745c5c3_Err != nil { | ||
| 1397 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 222, Col: 11} | 1557 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/components.templ`, Line: 240, Col: 11} |
| 1398 | } | 1558 | } | ||
| 1399 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) | 1559 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) |
| 1400 | if templ_7745c5c3_Err != nil { | 1560 | if templ_7745c5c3_Err != nil { | ||
| 1401 | return templ_7745c5c3_Err | 1561 | return templ_7745c5c3_Err | ||
| 1402 | } | 1562 | } | ||
| 1403 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "</span>") | 1563 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "</span>") |
| 1404 | if templ_7745c5c3_Err != nil { | 1564 | if templ_7745c5c3_Err != nil { | ||
| 1405 | return templ_7745c5c3_Err | 1565 | return templ_7745c5c3_Err | ||
| 1406 | } | 1566 | } | ||
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 generated file: collapsed
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> | ||