Commit 86007ab29b
86007ab29bc331fa3da95e3ffc0712d2feaaa63a
parent: 52243f111a
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-19 15:50 UTC
web: the repository header is two rows
Description, topics and website sit beside the name; the toggles hint is
title text on the buttons.
Ref #226
internal/httpd/repohead_test.go
added
+53
| @@ -0,0 +1,53 @@ |
| 1 | package httpd |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | |
| 7 | "gitbay.org/gitbay/internal/control" |
| 8 | "gitbay.org/gitbay/internal/web" |
| 9 | ) |
| 10 | |
| 11 | // The repository header is two rows: identity with the description and |
| 12 | // the buttons, then the tabs. The toggles hint is title text on the |
| 13 | // buttons, not a line of its own (desktop layout spec). |
| 14 | func TestRepoHeaderTwoRows(t *testing.T) { |
| 15 | var sb strings.Builder |
| 16 | p := testRepoPage() |
| 17 | p.Viewer = "alice" |
| 18 | p.Desc = "A CLI-first git forge." |
| 19 | p.Topics = []string{"cli"} |
| 20 | p.Tab = "files" |
| 21 | err := web.Render(&sb, "builds.html", struct { |
| 22 | repoPage |
| 23 | Builds []control.BuildOut |
| 24 | Jobs []control.JobOut |
| 25 | Runs []buildRun |
| 26 | Filter buildFilter |
| 27 | FilterLinks []buildFilterLink |
| 28 | Refs []string |
| 29 | CanWrite bool |
| 30 | Notice string |
| 31 | }{p, nil, nil, nil, buildFilter{}, nil, nil, true, ""}) |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | out := sb.String() |
| 36 | if strings.Contains(out, `class="toggles"`) || strings.Contains(out, "Pinned shows on your dashboard.") { |
| 37 | t.Error("the toggles hint still renders as a line") |
| 38 | } |
| 39 | for _, want := range []string{ |
| 40 | `title="Pinned repositories show on your dashboard"`, |
| 41 | `title="Watching sends every issue, request and build to your inbox"`, |
| 42 | `title="Bookmarked lists it under Bookmarks"`, |
| 43 | `<p class="repodesc">A CLI-first git forge.`, |
| 44 | } { |
| 45 | if !strings.Contains(out, want) { |
| 46 | t.Errorf("header lacks %q", want) |
| 47 | } |
| 48 | } |
| 49 | // the description sits inside the identity row, before the buttons |
| 50 | if strings.Index(out, `class="repodesc"`) > strings.Index(out, `action="/krz/gitbay/pin"`) { |
| 51 | t.Error("description renders after the buttons; it belongs in the identity row") |
| 52 | } |
| 53 | } |
internal/web/static/style.css
+8 −4
| @@ -412,13 +412,15 @@ footer a:hover { color: var(--link); } |
| 412 | 412 | .repotitle a:hover { color: var(--link); text-decoration: none; } |
| 413 | 413 | .repotitle .sep { color: var(--muted); margin: 0 2px; font-weight: 400; } |
| 414 | 414 | .repodesc { |
| 415 | | margin: var(--sp-2) 0 0; |
| 416 | | color: var(--fg); |
| 415 | margin: 0 0 0 var(--sp-2); |
| 416 | color: var(--muted); |
| 417 | 417 | font-size: var(--fs-2); |
| 418 | | display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; |
| 418 | display: inline-flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; |
| 419 | min-width: 0; |
| 419 | 420 | } |
| 421 | .repodesc a.site { color: var(--muted); } |
| 422 | .repodesc a.site:hover { color: var(--link); } |
| 420 | 423 | .repometa { margin: var(--sp-1) 0 0; color: var(--muted); font-size: var(--fs-1); } |
| 421 | | .toggles { margin: var(--sp-2) 0 0; color: var(--muted); font-size: var(--fs-1); } |
| 422 | 424 | |
| 423 | 425 | nav.tabs { |
| 424 | 426 | display: flex; |
| @@ -1518,6 +1520,8 @@ svg.icon { vertical-align: -0.125em; } |
| 1518 | 1520 | ones past the edge look absent. Two rows show all of them. */ |
| 1519 | 1521 | nav.tabs { flex-wrap: wrap; overflow: visible; } |
| 1520 | 1522 | .facts { grid-template-columns: 1fr; } |
| 1523 | /* a phone shows the description under the name, not beside it */ |
| 1524 | .repodesc { flex-basis: 100%; margin-left: 0; } |
| 1521 | 1525 | } |
| 1522 | 1526 | |
| 1523 | 1527 | @media (max-width: 40rem) { |
internal/web/templates/layout.html
+8 −12
| @@ -38,26 +38,22 @@ |
| 38 | 38 | {{with field . "Repo"}} |
| 39 | 39 | <header class="repohead"> |
| 40 | 40 | <div class="wrap"> |
| 41 | {{$top := topTab (str $ "Tab")}} |
| 41 | 42 | <div class="identity"> |
| 42 | 43 | {{if field $ "RepoHome"}}<h1 class="repotitle"><a class="owner" href="/{{.OwnerName}}">{{.OwnerName}}</a><span class="sep">/</span>{{.Name}}</h1> |
| 43 | 44 | {{else}}<p class="repotitle"><a class="owner" href="/{{.OwnerName}}">{{.OwnerName}}</a><span class="sep">/</span><a href="/{{.OwnerName}}/{{.Name}}">{{.Name}}</a></p>{{end}} |
| 44 | 45 | {{if eq .Visibility "private"}}<span class="chip">Private</span>{{end}} |
| 45 | 46 | {{if .Settings.Archived}}<span class="chip">Archived</span>{{end}} |
| 47 | {{/* Description, topics and website belong to the code tab, inline |
| 48 | with the name so the header is two rows on every page. */}} |
| 49 | {{if eq $top "code"}}{{if or (field $ "Desc") (field $ "Topics") $.Repo.Settings.Website}}<p class="repodesc">{{with field $ "Desc"}}{{.}}{{end}} {{with field $ "Topics"}}{{range .}}<a class="chip topic" href="/explore?q={{.}}">{{.}}</a> {{end}}{{end}}{{with $.Repo.Settings.Website}}<a class="site" href="{{.}}" rel="nofollow">{{.}}</a>{{end}}</p>{{end}}{{end}} |
| 46 | 50 | <span class="grow"></span> |
| 47 | | {{if $.Viewer}}<form method="post" action="/{{.OwnerName}}/{{.Name}}/pin" class="inline"><button type="submit" class="btn" aria-pressed="{{if field $ "Pinned"}}true{{else}}false{{end}}"><span aria-hidden="true">{{if field $ "Pinned"}}★{{else}}☆{{end}}</span> {{if field $ "Pinned"}}Pinned{{else}}Pin{{end}}</button></form> |
| 48 | | <form method="post" action="/{{.OwnerName}}/{{.Name}}/watch" class="inline"><button type="submit" class="btn" aria-pressed="{{if eq (str $ "Watch") "watching"}}true{{else}}false{{end}}">{{if eq (str $ "Watch") "watching"}}Watching{{else}}Watch{{end}}</button></form> |
| 49 | | <form method="post" action="/{{.OwnerName}}/{{.Name}}/bookmark" class="inline"><button type="submit" class="btn" aria-pressed="{{if field $ "Marked"}}true{{else}}false{{end}}">{{if field $ "Marked"}}Bookmarked{{else}}Bookmark{{end}}</button></form> |
| 51 | {{if $.Viewer}}<form method="post" action="/{{.OwnerName}}/{{.Name}}/pin" class="inline"><button type="submit" class="btn" aria-pressed="{{if field $ "Pinned"}}true{{else}}false{{end}}" title="Pinned repositories show on your dashboard"><span aria-hidden="true">{{if field $ "Pinned"}}★{{else}}☆{{end}}</span> {{if field $ "Pinned"}}Pinned{{else}}Pin{{end}}</button></form> |
| 52 | <form method="post" action="/{{.OwnerName}}/{{.Name}}/watch" class="inline"><button type="submit" class="btn" aria-pressed="{{if eq (str $ "Watch") "watching"}}true{{else}}false{{end}}" title="Watching sends every issue, request and build to your inbox">{{if eq (str $ "Watch") "watching"}}Watching{{else}}Watch{{end}}</button></form> |
| 53 | <form method="post" action="/{{.OwnerName}}/{{.Name}}/bookmark" class="inline"><button type="submit" class="btn" aria-pressed="{{if field $ "Marked"}}true{{else}}false{{end}}" title="Bookmarked lists it under Bookmarks">{{if field $ "Marked"}}Bookmarked{{else}}Bookmark{{end}}</button></form> |
| 50 | 54 | <form method="post" action="/{{.OwnerName}}/{{.Name}}/fork" class="inline"><button type="submit" class="btn">Fork</button></form>{{end}} |
| 51 | 55 | </div> |
| 52 | | {{$top := topTab (str $ "Tab")}} |
| 53 | | {{/* Description, topics and metadata belong to the code tab. On task |
| 54 | | tabs only the identity row and the tab bar render, and the header |
| 55 | | is identical on every page within a tab. */}} |
| 56 | | {{if eq $top "code"}} |
| 57 | | {{if or (field $ "Desc") (field $ "Topics")}}<p class="repodesc">{{with field $ "Desc"}}{{.}}{{end}} {{with field $ "Topics"}}{{range .}}<a class="chip topic" href="/explore?q={{.}}">{{.}}</a> {{end}}{{end}}</p>{{end}} |
| 58 | | {{if or $.Repo.Settings.Website (field $ "Mirrors")}}<p class="repometa">{{with $.Repo.Settings.Website}}<a href="{{.}}" rel="nofollow">{{.}}</a>{{end}}{{range $i, $m := field $ "Mirrors"}}{{if or $i $.Repo.Settings.Website}} · {{end}}{{if eq $m.Direction "push"}}mirrors to{{else}}mirrors from{{end}} <a href="{{$m.URL}}" rel="nofollow">{{$m.Target}}</a>{{if $m.Error}}, <span class="bad">sync error: {{$m.Error}}</span>{{else if $m.Synced}}, synced {{$m.Synced}}{{end}}{{end}}</p>{{end}} |
| 59 | | {{if $.Viewer}}<p class="toggles">Pinned shows on your dashboard. Watching sends every issue, request and build to your inbox. Bookmarked lists it under Bookmarks.</p>{{end}} |
| 60 | | {{end}} |
| 56 | {{if eq $top "code"}}{{if field $ "Mirrors"}}<p class="repometa">{{range $i, $m := field $ "Mirrors"}}{{if $i}} · {{end}}{{if eq $m.Direction "push"}}mirrors to{{else}}mirrors from{{end}} <a href="{{$m.URL}}" rel="nofollow">{{$m.Target}}</a>{{if $m.Error}}, <span class="bad">sync error: {{$m.Error}}</span>{{else if $m.Synced}}, synced {{$m.Synced}}{{end}}{{end}}</p>{{end}}{{end}} |
| 61 | 57 | <nav class="tabs" aria-label="Repository"> |
| 62 | 58 | <a {{if eq $top "code"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}">Code</a> |
| 63 | 59 | <a {{if eq $top "issues"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}/issues">Issues{{with field $ "OpenIssues"}}{{if .}} <i>{{.}}</i>{{end}}{{end}}</a> |