A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit 027899f05d

027899f05d625769c881aeac38b7ef6bbfe4fa4f

parent: ba046389a8

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-26T00:47:42Z

web: rail for cross-repo state, repo header once, horizontal tabs

The previous two passes restyled the chrome and left the pages alone: the
repo header, description and topics rendered ahead of content on all
twelve repo pages, the page's own subject was never its h1, and content
sat in 990px with 247px of unused margin beside it.

- the rail now holds what changes as you switch context (pinned repos,
  merge requests waiting on your review) instead of the nine repo links,
  which were a fixed set and belong in tabs. Repo navigation moves to a
  horizontal tab bar with open counts.
- the full repo header — description, topics, website, mirrors — renders
  only on the repo home. Every other page gets identity and tabs.
- each page supplies its own h1; the repo name is the h1 only on the repo
  home. Rail group captions are <p>, not headings, so they no longer sit
  ahead of the page h1 in the outline.
- skip link, authored focus ring, aria-current on the active tab and rail
  entry. Mobile collapses the rail to one strip: chrome ahead of content
  drops from ~600px to 141px on a 375px viewport.
- Harbor palette: black and white ground, blue for what you can do, orange
  for where you are and what wants you. Fills keep pure #0000f0 in both
  schemes; blue text lifts to #5b6bff on dark, where #0000f0 is 2.13:1.
- IBM Plex gives way to Atkinson Hyperlegible, whose letterforms are drawn
  so 0/O and 1/l cannot collapse — this ships a real italic, which the
  three Plex faces never had, so emphasis in READMEs and issue bodies was
  a synthesized oblique.

Tests updated where they assert on display copy that changed case.
e2e/design_test.go +3 −3
@@ -58,7 +58,7 @@ func TestReadmeRelativeLinks(t *testing.T) {
5858 }
5959 }
6060 // Branch dropdown lists branches.
61 if !strings.Contains(body, `class="refmenu"`) || !strings.Contains(body, ">all refs") {
61 if !strings.Contains(body, `class="refmenu"`) || !strings.Contains(body, ">All refs") {
6262 t.Error("branch dropdown missing")
6363 }
6464 // Per-file history: ?path= filters the log; blob pages link to it.
@@ -164,14 +164,14 @@ func TestWebInteractions(t *testing.T) {
164164 t.Fatal("pin toggle failed")
165165 }
166166 _, body := browserGet(t, browser, inst.base()+"/theorg/webborn")
167 if !strings.Contains(body, "★ pinned") {
167 if !strings.Contains(body, "★ Pinned") {
168168 t.Fatal("repo header not pinned")
169169 }
170170 if _, body = browserGet(t, browser, inst.base()+"/"); !strings.Contains(body, "theorg<span") {
171171 t.Fatal("dashboard missing pinned repo")
172172 }
173173 browserPost(t, browser, inst.base()+"/theorg/webborn/pin", url.Values{})
174 if _, body = browserGet(t, browser, inst.base()+"/theorg/webborn"); !strings.Contains(body, "☆ pin") {
174 if _, body = browserGet(t, browser, inst.base()+"/theorg/webborn"); !strings.Contains(body, "☆ Pin") {
175175 t.Fatal("unpin failed")
176176 }
177177
e2e/mirror_test.go +2 −2
@@ -119,10 +119,10 @@ func TestMirrors(t *testing.T) {
119119 if status, _ := browserGet(t, browser, inst.base()+loginPath); status != 200 {
120120 t.Fatalf("login: %d", status)
121121 }
122 if _, body := browserGet(t, browser, inst.base()+"/alice/app"); !strings.Contains(body, "mirrors to <a href=\""+remoteURL) {
122 if _, body := browserGet(t, browser, inst.base()+"/alice/app"); !strings.Contains(body, "Mirrors to <a href=\""+remoteURL) {
123123 t.Fatalf("admin repo page missing mirror line:\n%s", body)
124124 }
125 if _, body := browserGet(t, newBrowser(t), inst.base()+"/alice/app"); strings.Contains(body, "mirrors to") {
125 if _, body := browserGet(t, newBrowser(t), inst.base()+"/alice/app"); strings.Contains(body, "Mirrors to") {
126126 t.Fatalf("anonymous repo page shows mirror line:\n%s", body)
127127 }
128128
e2e/web_test.go +1 −1
@@ -88,7 +88,7 @@ func TestWebUI(t *testing.T) {
8888 if !strings.Contains(body, "<h1>hello site</h1>") || !strings.Contains(body, "<em>markdown</em>") {
8989 t.Fatalf("README not rendered:\n%s", body)
9090 }
91 for _, tab := range []string{">issues<", ">merge requests<"} {
91 for _, tab := range []string{">Issues<", ">Merge requests<"} {
9292 if !strings.Contains(body, tab) {
9393 t.Fatalf("repo header missing %s tab", tab)
9494 }
e2e/wiki_test.go +2 −2
@@ -23,7 +23,7 @@ func TestWikis(t *testing.T) {
2323 // No wiki yet: the tab is absent, the page shows the push hint, and
2424 // cloning the companion says so.
2525 _, body := inst.get(t, "/alice/app")
26 if strings.Contains(body, ">wiki<") {
26 if strings.Contains(body, ">Wiki<") {
2727 t.Fatal("wiki tab shown with no wiki")
2828 }
2929 _, body = inst.get(t, "/alice/app/wiki")
@@ -55,7 +55,7 @@ func TestWikis(t *testing.T) {
5555 // Rendering: home resolves, tab appears, links rewrite to wiki pages
5656 // and images to the wiki raw route; org pages render too.
5757 _, body = inst.get(t, "/alice/app")
58 if !strings.Contains(body, ">wiki<") {
58 if !strings.Contains(body, ">Wiki<") {
5959 t.Fatal("wiki tab missing after push")
6060 }
6161 _, body = inst.get(t, "/alice/app/wiki")
internal/httpd/accounts.go +18 −23
@@ -64,19 +64,18 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
6464 token := r.URL.Query().Get("token")
6565 if token == "" {
6666 s.render(w, "login.html", struct {
67 Site string
68 Viewer string
69 Error string
70 }{s.siteName(), "", ""})
67 basePage
68 Error string
69 }{basePage{Site: s.siteName()}, ""})
7170 return
7271 }
7372 userID, err := s.st.ConsumeLoginToken(store.HashToken(token))
7473 if err != nil {
7574 s.render(w, "login.html", struct {
76 Site string
77 Viewer string
78 Error string
79 }{s.siteName(), "", "that login link is invalid, expired, or already used — mint a new one"})
75 basePage
76 Error string
77 }{basePage{Site: s.siteName()},
78 "that login link is invalid, expired, or already used — mint a new one"})
8079 return
8180 }
8281 sessTok, sessHash, err := store.NewToken()
@@ -120,11 +119,10 @@ func (s *Server) adminOrgs(u store.User) []string {
120119
121120 func (s *Server) renderNewRepo(w http.ResponseWriter, u store.User, errMsg string) {
122121 s.render(w, "new.html", struct {
123 Site string
124 Viewer string
125 Orgs []string
126 Error string
127 }{s.siteName(), u.Username, s.adminOrgs(u), errMsg})
122 basePage
123 Orgs []string
124 Error string
125 }{s.baseFor(u), s.adminOrgs(u), errMsg})
128126 }
129127
130128 func (s *Server) newRepoForm(w http.ResponseWriter, r *http.Request, u store.User) {
@@ -223,13 +221,12 @@ func (s *Server) signupForm(w http.ResponseWriter, r *http.Request) {
223221
224222 func (s *Server) renderSignup(w http.ResponseWriter, errMsg, username string) {
225223 s.render(w, "register.html", struct {
226 Site string
227 Viewer string
224 basePage
228225 Host string
229226 Mode string // open | invite
230227 Error string
231228 Username string
232 }{s.siteName(), "", s.cfg.SiteHost(), s.cfg.Registration.Mode, errMsg, username})
229 }{basePage{Site: s.siteName()}, s.cfg.SiteHost(), s.cfg.Registration.Mode, errMsg, username})
233230 }
234231
235232 func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) {
@@ -247,12 +244,11 @@ func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) {
247244 return
248245 }
249246 s.render(w, "registered.html", struct {
250 Site string
251 Viewer string
247 basePage
252248 Username string
253249 Message string
254250 Host string
255 }{s.siteName(), "", username, msg, s.cfg.SiteHost()})
251 }{basePage{Site: s.siteName()}, username, msg, s.cfg.SiteHost()})
256252 }
257253
258254 // issueCreateForm renders the new-issue form, prefilled from the repo's
@@ -441,8 +437,7 @@ func (s *Server) mrCommentSubmit(w http.ResponseWriter, r *http.Request, u store
441437 }
442438
443439 type editPage struct {
444 Site string
445 Viewer string
440 basePage
446441 Repo store.Repo
447442 Ref string
448443 Path string
@@ -467,7 +462,7 @@ func (s *Server) editForm(w http.ResponseWriter, r *http.Request, u store.User)
467462 return
468463 }
469464 s.render(w, "edit.html", editPage{
470 Site: s.siteName(), Viewer: u.Username, Repo: repo,
465 basePage: s.baseFor(u), Repo: repo,
471466 Ref: ref, Path: filePath, Content: string(content),
472467 })
473468 }
@@ -481,7 +476,7 @@ func (s *Server) editSubmit(w http.ResponseWriter, r *http.Request, u store.User
481476 filePath := strings.Trim(r.PathValue("path"), "/")
482477 fail := func(msg string) {
483478 s.render(w, "edit.html", editPage{
484 Site: s.siteName(), Viewer: u.Username, Repo: repo,
479 basePage: s.baseFor(u), Repo: repo,
485480 Ref: ref, Path: filePath, Content: r.FormValue("content"), Error: msg,
486481 })
487482 }
internal/httpd/page.go added +85
@@ -0,0 +1,85 @@
1package httpd
2
3import (
4 "net/http"
5
6 "gitbay.org/gitbay/internal/policy"
7 "gitbay.org/gitbay/internal/store"
8)
9
10// railRepo is one pinned repository in the rail.
11type railRepo struct {
12 Owner string
13 Name string
14}
15
16// railItem is one merge request in the rail's review queue.
17type railItem struct {
18 RepoPath string
19 Number int64
20 Title string
21}
22
23// rail is the cross-repo state in the left rail: the repositories and the
24// review work a viewer moves between. A repo's own navigation lives in its
25// header tabs, not here — the rail holds only what changes as you switch
26// context, which is what earns it the width.
27type rail struct {
28 Pinned []railRepo
29 Reviews []railItem
30}
31
32// Empty reports whether the rail has nothing to show beyond the global
33// links, so the template can skip its group headings.
34func (r rail) Empty() bool { return len(r.Pinned) == 0 && len(r.Reviews) == 0 }
35
36// basePage is what the layout needs on every page, repo or not. Page
37// structs embed it so the rail and the site name are always in scope.
38type basePage struct {
39 Site string
40 Viewer string
41 Rail rail
42}
43
44// base builds the layout-wide data for a request that has not already
45// resolved a viewer.
46func (s *Server) base(r *http.Request) basePage {
47 if s.cfg.Web.Mode != "accounts" {
48 return basePage{Site: s.siteName()}
49 }
50 return s.baseFor(s.viewer(r))
51}
52
53// baseFor is base for a handler that already holds the viewer, so the
54// session lookup is not repeated.
55func (s *Server) baseFor(viewer store.User) basePage {
56 b := basePage{Site: s.siteName()}
57 if viewer.ID == 0 {
58 return b
59 }
60 b.Viewer = viewer.Username
61 b.Rail = s.railFor(viewer)
62 return b
63}
64
65// railFor collects the viewer's pinned repositories and review queue,
66// dropping anything they may no longer read.
67func (s *Server) railFor(viewer store.User) rail {
68 var rl rail
69 pinned, _ := s.st.PinnedRepos(viewer.ID)
70 for _, rp := range pinned {
71 grant, _ := s.st.AccessRole(rp.ID, viewer.ID)
72 if policy.CanRead(viewer, rp, grant) {
73 rl.Pinned = append(rl.Pinned, railRepo{Owner: rp.OwnerName, Name: rp.Name})
74 }
75 }
76 queue, _ := s.st.ReviewQueue(viewer.ID)
77 for _, q := range queue {
78 rl.Reviews = append(rl.Reviews, railItem{
79 RepoPath: q.RepoPath,
80 Number: q.Number,
81 Title: q.Title,
82 })
83 }
84 return rl
85}
internal/httpd/web.go +40 −37
@@ -62,8 +62,8 @@ func (s *Server) favicon(w http.ResponseWriter, r *http.Request) {
6262 w.Write(web.FaviconSVG)
6363 }
6464
65// font serves the embedded IBM Plex subsets. Same-origin, so the CSP's
66// default-src 'self' covers it — no font CDN.
65// font serves the embedded Atkinson Hyperlegible subsets. Same-origin,
66// so the CSP's default-src 'self' covers it — no font CDN.
6767 func (s *Server) font(w http.ResponseWriter, r *http.Request) {
6868 data, err := web.FontFS.ReadFile("static" + r.URL.Path[len("/static"):])
6969 if err != nil {
@@ -79,10 +79,9 @@ func (s *Server) font(w http.ResponseWriter, r *http.Request) {
7979 // the stock plain-text response if the template fails.
8080 func (s *Server) notFound(w http.ResponseWriter, r *http.Request) {
8181 var buf bytes.Buffer
82 if err := web.Render(&buf, "404.html", struct {
83 Site string
84 Viewer string
85 }{s.siteName(), s.viewerName(r)}); err != nil {
82 if err := web.Render(&buf, "404.html", basePage{
83 Site: s.siteName(), Viewer: s.viewerName(r),
84 }); err != nil {
8685 http.NotFound(w, r)
8786 return
8887 }
@@ -129,12 +128,11 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) {
129128 host := strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(
130129 s.cfg.Server.SiteURL, "https://"), "http://"), "/")
131130 s.render(w, "landing.html", struct {
132 Site string
133 Viewer string
131 basePage
134132 Host string
135133 Accounts bool
136134 Signup bool
137 }{s.siteName(), "", host, s.cfg.Web.Mode == "accounts",
135 }{basePage{Site: s.siteName()}, host, s.cfg.Web.Mode == "accounts",
138136 s.cfg.Web.Mode == "accounts" && s.cfg.Registration.Mode != "closed"})
139137 }
140138
@@ -150,12 +148,11 @@ func (s *Server) dashboard(w http.ResponseWriter, r *http.Request, viewer store.
150148 mrs, _ := s.st.DashboardMRs(viewer.ID)
151149 issues, _ := s.st.DashboardIssues(viewer.ID)
152150 s.render(w, "dashboard.html", struct {
153 Site string
154 Viewer string
151 basePage
155152 Pinned []store.Repo
156153 MRs []store.DashboardItem
157154 Issues []store.DashboardItem
158 }{s.siteName(), viewer.Username, visible, mrs, issues})
155 }{s.baseFor(viewer), visible, mrs, issues})
159156 }
160157
161158 func (s *Server) explore(w http.ResponseWriter, r *http.Request) {
@@ -170,11 +167,10 @@ func (s *Server) explore(w http.ResponseWriter, r *http.Request) {
170167 }
171168 q := strings.TrimSpace(r.URL.Query().Get("q"))
172169 s.render(w, "explore.html", struct {
173 Site string
174 Viewer string
175 Query string
176 Repos []describedRepo
177 }{s.siteName(), viewer.Username, q, s.filterRepos(q, s.describeAll(repos))})
170 basePage
171 Query string
172 Repos []describedRepo
173 }{s.baseFor(viewer), q, s.filterRepos(q, s.describeAll(repos))})
178174 }
179175
180176 // viewerName returns the logged-in username for header rendering, or "".
@@ -189,11 +185,10 @@ func (s *Server) viewerName(r *http.Request) string {
189185 // data, plus this instance's operator-provided notes.
190186 func (s *Server) privacy(w http.ResponseWriter, r *http.Request) {
191187 s.render(w, "privacy.html", struct {
192 Site string
193 Viewer string
188 basePage
194189 Host string
195190 Notice string
196 }{s.siteName(), s.viewerName(r), s.cfg.SiteHost(), s.cfg.Web.PrivacyNotice})
191 }{s.base(r), s.cfg.SiteHost(), s.cfg.Web.PrivacyNotice})
197192 }
198193
199194 // filterRepos keeps repos whose path, description, or topics contain the
@@ -222,8 +217,7 @@ func (s *Server) filterRepos(q string, repos []describedRepo) []describedRepo {
222217
223218 // repoPage is the shared context for repo-scoped pages.
224219 type repoPage struct {
225 Site string
226 Viewer string
220 basePage
227221 Desc string
228222 Repo store.Repo
229223 Ref string
@@ -235,6 +229,13 @@ type repoPage struct {
235229 HasWiki bool
236230 Host string
237231 Mirrors []mirrorLine // repo admins only
232 // OpenIssues and OpenMRs are the counts on the header tabs.
233 OpenIssues int
234 OpenMRs int
235 // RepoHome asks the layout for the full header — description, topics,
236 // website, mirrors. Every other page gets identity and tabs only, so a
237 // repo describes itself once rather than on all twelve of its pages.
238 RepoHome bool
238239 }
239240
240241 // mirrorLine is the admin-only mirror status shown in the repo header.
@@ -300,19 +301,21 @@ func (s *Server) repoFor(w http.ResponseWriter, r *http.Request, ref string) (re
300301 })
301302 }
302303 }
304 openIssues, openMRs := s.st.OpenCounts(repo.ID)
303305 return repoPage{
304 Mirrors: mirrors,
305 Site: s.siteName(),
306 Viewer: viewer.Username,
307 Pinned: pinned,
308 HasWiki: s.wikiDir(repo.OwnerName, repo.Name) != "",
309 Host: s.cfg.SiteHost(),
310 Desc: gitutil.ReadDescription(control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name)),
311 Repo: repo,
312 Ref: ref,
313 CloneURL: s.cfg.Server.SiteURL + "/" + repo.Path() + ".git",
314 Dir: control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name),
315 Topics: topics,
306 basePage: s.baseFor(viewer),
307 Mirrors: mirrors,
308 Pinned: pinned,
309 HasWiki: s.wikiDir(repo.OwnerName, repo.Name) != "",
310 Host: s.cfg.SiteHost(),
311 Desc: gitutil.ReadDescription(control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name)),
312 Repo: repo,
313 Ref: ref,
314 CloneURL: s.cfg.Server.SiteURL + "/" + repo.Path() + ".git",
315 Dir: control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name),
316 Topics: topics,
317 OpenIssues: openIssues,
318 OpenMRs: openMRs,
316319 }, true
317320 }
318321
@@ -385,8 +388,7 @@ func (s *Server) ownerPage(w http.ResponseWriter, r *http.Request) {
385388 weeks, activityTotal := activityGrid(counts)
386389
387390 s.render(w, "owner.html", struct {
388 Site string
389 Viewer string
391 basePage
390392 Owner string
391393 Kind string
392394 Profile store.Profile
@@ -395,7 +397,7 @@ func (s *Server) ownerPage(w http.ResponseWriter, r *http.Request) {
395397 Orgs []store.OrgMember
396398 Activity []activityWeek
397399 ActivityTotal int
398 }{s.siteName(), viewer.Username, name, kind, profile, s.describeAll(visible), members, orgs,
400 }{s.baseFor(viewer), name, kind, profile, s.describeAll(visible), members, orgs,
399401 weeks, activityTotal})
400402 }
401403
@@ -405,6 +407,7 @@ func (s *Server) repoHome(w http.ResponseWriter, r *http.Request) {
405407 return
406408 }
407409 p.Tab = "files"
410 p.RepoHome = true
408411 s.renderTree(w, r, p, "")
409412 }
410413
internal/store/dashboard.go +31
@@ -118,3 +118,34 @@ func (s *Store) PinnedRepos(userID int64) ([]Repo, error) {
118118 }
119119 return out, rows.Err()
120120 }
121
122// ReviewQueue returns open merge requests the user is involved in, has not
123// authored, and has not reviewed at the current head — what the rail shows
124// as waiting on them. Ordered most recently touched first.
125func (s *Store) ReviewQueue(userID int64) ([]DashboardItem, error) {
126 return s.dashboardQuery(`
127 SELECT COALESCE(u.username, o.name) || '/' || r.name,
128 x.number, x.title, au.username, x.state, x.updated_at
129 FROM merge_requests x
130 JOIN repos r ON r.id = x.repo_id
131 LEFT JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id
132 LEFT JOIN orgs o ON r.owner_kind = 'org' AND o.id = r.owner_id
133 JOIN users au ON au.id = x.author_id
134 WHERE x.state IN ('open', 'source_gone')
135 AND x.author_id <> ?1
136 AND NOT EXISTS (SELECT 1 FROM mr_reviews rv
137 WHERE rv.mr_id = x.id AND rv.reviewer_id = ?1
138 AND rv.head_sha = x.head_sha)
139 AND `+involvedCond+`
140 ORDER BY x.updated_at DESC LIMIT 8`, userID)
141}
142
143// OpenCounts returns the repo's open issue and open merge request counts,
144// for the repo tab badges.
145func (s *Store) OpenCounts(repoID int64) (issues, mrs int) {
146 s.DB.QueryRow("SELECT COUNT(*) FROM issues WHERE repo_id = ? AND state = 'open'",
147 repoID).Scan(&issues)
148 s.DB.QueryRow("SELECT COUNT(*) FROM merge_requests WHERE repo_id = ? AND state IN ('open', 'source_gone')",
149 repoID).Scan(&mrs)
150 return
151}
internal/web/static/favicon.svg +1 −1
@@ -1 +1 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="1" y="1" width="22" height="22" rx="6" fill="#0000f0"/><path d="M5 9.5c2-2.5 4-2.5 6 0s4 2.5 6 0" stroke="#ffffff" stroke-width="2.2" fill="none" stroke-linecap="round"/><path d="M5 15c2-2.5 4-2.5 6 0s4 2.5 6 0" stroke="#ffffff" stroke-width="2.2" fill="none" stroke-linecap="round"/></svg>
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" fill="#000000"/><path d="M12 2.25 18.75 12H5.25z" fill="#ff6b3d"/><rect x="3" y="13.5" width="18" height="3" fill="#ffffff"/><rect x="7.5" y="18" width="9" height="3" fill="#ffffff"/></svg>
internal/web/static/fonts/atkinson-mono-roman.woff2 added

Binary file not shown.

internal/web/static/fonts/atkinson-next-italic.woff2 added

Binary file not shown.

internal/web/static/fonts/atkinson-next-roman.woff2 added

Binary file not shown.

internal/web/static/fonts/plex-mono-400.woff2 deleted

Binary file not shown.

internal/web/static/fonts/plex-mono-500.woff2 deleted

Binary file not shown.

internal/web/static/fonts/plex-sans.woff2 deleted

Binary file not shown.

internal/web/static/style.css +287 −259
@@ -1,28 +1,31 @@
1/* gitbay stylesheet, Carbon-school: black shell, sharp lines, one blue.
2 One file, light and dark via prefers-color-scheme. Tokens first;
3 components use tokens only. No external requests: IBM Plex is served
4 from this origin. */
1/* gitbay stylesheet — Harbor: black and white ground, two accents with
2 separate jobs. Blue is what you can do (links, buttons, focus); orange
3 is where you are and what wants you (active nav, counts, pending state,
4 unverified signatures). Orange is never a button, so the two never
5 compete. One file, light and dark via prefers-color-scheme. Tokens
6 first; components use tokens only. No external requests: Atkinson
7 Hyperlegible is served from this origin. */
58
69 @font-face {
7 font-family: "IBM Plex Sans";
10 font-family: "Atkinson Hyperlegible Next";
811 font-style: normal;
9 font-weight: 400 600;
12 font-weight: 400 700;
1013 font-display: swap;
11 src: url(/static/fonts/plex-sans.woff2) format("woff2");
14 src: url(/static/fonts/atkinson-next-roman.woff2) format("woff2");
1215 }
1316 @font-face {
14 font-family: "IBM Plex Mono";
15 font-style: normal;
16 font-weight: 400;
17 font-family: "Atkinson Hyperlegible Next";
18 font-style: italic;
19 font-weight: 400 700;
1720 font-display: swap;
18 src: url(/static/fonts/plex-mono-400.woff2) format("woff2");
21 src: url(/static/fonts/atkinson-next-italic.woff2) format("woff2");
1922 }
2023 @font-face {
21 font-family: "IBM Plex Mono";
24 font-family: "Atkinson Hyperlegible Mono";
2225 font-style: normal;
23 font-weight: 500;
26 font-weight: 400 600;
2427 font-display: swap;
25 src: url(/static/fonts/plex-mono-500.woff2) format("woff2");
28 src: url(/static/fonts/atkinson-mono-roman.woff2) format("woff2");
2629 }
2730
2831 :root {
@@ -30,39 +33,50 @@
3033 scrollbars per scheme, and opt out of browser auto-darkening */
3134 color-scheme: light dark;
3235
33 /* palette (Carbon g10) */
34 --bg: #f4f4f4;
35 --fg: #161616;
36 --muted: #525252;
37 --faint: #ececec; /* hairlines, row separators */
38 --line: #e0e0e0; /* control borders, tile edges */
39 --surface: #ffffff; /* tiles, cards */
40 --hover: #f0f2f8; /* row hover on tiles */
41 --accent: #0f62fe; /* blue-60: links, active nav, primary */
42 --accent-fg: #ffffff; /* text on accent */
43 --ok: #198038;
44 --warn: #8e6a00;
45 --bad: #da1e28;
46 --done: #8a3ffc; /* merged */
47 --neutral: #525252;
48 --code-bg: #f4f4f4;
49 /* the shell is ink in both schemes */
50 --shell-bg: #161616;
51 --shell-fg: #f4f4f4;
52 --shell-muted: #c6c6c6;
53 --shell-line: #161616;
36 /* ground and ink */
37 --bg: #ffffff;
38 --fg: #0d0d0d;
39 --muted: #595959; /* 7.00:1 on white */
40 --faint: #eaeaea; /* hairlines, row separators */
41 --line: #d4d4d4; /* control borders, table edges */
42 --surface: #ffffff;
43 --hover: #f5f5f5;
44 --code-bg: #f5f5f5;
45
46 /* blue: what you can do */
47 --accent: #0000f0; /* links, focus — 9.30:1 on white */
48 --accent-fg: #ffffff;
49 --fill: #0000f0; /* button grounds; white on it is 9.30:1 */
50
51 /* orange: where you are, what wants you */
52 --mark: #e4572e; /* bars and underlines — non-text, 3.0 bar */
53 --warn: #c2410c; /* the same orange at text size — 5.18:1 */
54
55 /* semantic */
56 --ok: #0a7d3c;
57 --bad: #c62828;
58 --done: #6b21a8; /* merged */
59 --neutral: #595959;
60
61 /* the rail is black in both schemes: the one brand constant */
62 --shell-bg: #000000;
63 --shell-fg: #f2f2f2;
64 --shell-muted: #8f8f8f;
65 --shell-line: #1c1c1c;
66 --shell-hover: #141414;
67 --shell-mark: #ff6b3d; /* 7.42:1 on black, both schemes */
5468
5569 /* type */
56 --sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
57 Roboto, "Helvetica Neue", Arial, sans-serif;
58 --mono: "IBM Plex Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo,
59 Consolas, "Liberation Mono", monospace;
60 --fs-0: 0.8rem; /* 12.8px: chips, fine print */
61 --fs-1: 0.866rem; /* ~13px: metadata, mono in tables */
62 --fs-2: 0.933rem; /* 14px: dense table text */
63 --fs-3: 1rem; /* 15px: body */
64 --fs-4: 1.135rem; /* ~17px: h2 */
65 --fs-5: 1.335rem; /* ~20px: h1 */
70 --sans: "Atkinson Hyperlegible Next", -apple-system, BlinkMacSystemFont,
71 "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
72 --mono: "Atkinson Hyperlegible Mono", ui-monospace, "SF Mono",
73 SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
74 --fs-0: 0.78rem;
75 --fs-1: 0.85rem;
76 --fs-2: 0.92rem;
77 --fs-3: 1rem;
78 --fs-4: 1.2rem;
79 --fs-5: 1.6rem;
6680
6781 /* spacing */
6882 --sp-1: 0.25rem;
@@ -72,31 +86,31 @@
7286 --sp-5: 1.5rem;
7387 --sp-6: 2.5rem;
7488
75 /* radii: sharp everywhere; only tags stay pills */
76 --r-sm: 0;
77 --r-md: 0;
78 --r-lg: 0;
79 --r-pill: 999px;
89 /* radii: 2px everywhere — chart plates, not pills */
90 --r-sm: 2px;
91 --r-md: 2px;
92 --r-lg: 2px;
93 --r-pill: 2px;
8094 }
8195 @media (prefers-color-scheme: dark) {
8296 :root {
83 /* Carbon g100 */
84 --bg: #161616;
85 --fg: #f4f4f4;
86 --muted: #a8a8a8;
87 --faint: #2a2a2a;
88 --line: #393939;
89 --surface: #262626;
90 --hover: #2e3440;
91 --accent: #78a9ff; /* blue-40 reads on dark grounds */
92 --accent-fg: #161616;
93 --ok: #42be65;
94 --warn: #d2a106;
95 --bad: #fa4d56;
96 --done: #a56eff;
97 --neutral: #a8a8a8;
98 --code-bg: #131313;
99 --shell-line: #393939;
97 --bg: #0a0a0a;
98 --fg: #f2f2f2;
99 --muted: #a1a1a1; /* 7.66:1 on #0a0a0a */
100 --faint: #1c1c1c;
101 --line: #2e2e2e;
102 --surface: #0f0f0f;
103 --hover: #161616;
104 --code-bg: #050505;
105 --accent: #5b6bff; /* 4.71:1 — pure #0000f0 is 2.13:1 here */
106 --accent-fg: #ffffff;
107 --fill: #0000f0; /* fills stay pure; white on it clears 9:1 */
108 --mark: #ff6b3d;
109 --warn: #ff6b3d;
110 --ok: #3fce7a;
111 --bad: #ff6b6b;
112 --done: #c084fc;
113 --neutral: #a1a1a1;
100114 }
101115 }
102116
@@ -108,18 +122,46 @@ body {
108122 color: var(--fg);
109123 font: var(--fs-3)/1.6 var(--sans);
110124 min-height: 100vh;
111 display: flex;
112 flex-direction: column;
113125 }
114main { flex: 1; }
115126
116127 a { color: var(--accent); text-decoration: none; }
117128 a:hover { text-decoration: underline; }
118129
119h1 { font-size: var(--fs-5); font-weight: 600; line-height: 1.3;
120 margin: var(--sp-5) 0 var(--sp-3); letter-spacing: -0.01em; }
121h2 { font-size: var(--fs-4); font-weight: 600; line-height: 1.35;
122 margin: var(--sp-5) 0 var(--sp-3); }
130/* one authored focus ring, so keyboard position is never the browser's
131 guess. Blue, because focus is an interactive state. */
132:focus-visible {
133 outline: 2px solid var(--accent);
134 outline-offset: 2px;
135 border-radius: 1px;
136}
137
138/* visually hidden, still read aloud: for headings a sighted reader gets
139 from the layout but a screen reader would otherwise miss */
140.vh {
141 position: absolute;
142 width: 1px; height: 1px;
143 margin: -1px; padding: 0; border: 0;
144 clip-path: inset(50%);
145 overflow: hidden;
146 white-space: nowrap;
147}
148/* the rail runs ahead of the content in source order, so give keyboard
149 and screen-reader users a way past it */
150a.skip {
151 position: absolute;
152 left: var(--sp-3); top: -3rem;
153 z-index: 50;
154 background: var(--fill);
155 color: #ffffff;
156 padding: var(--sp-2) var(--sp-4);
157 font-weight: 600;
158}
159a.skip:focus { top: var(--sp-3); text-decoration: none; }
160
161h1 { font-size: var(--fs-5); font-weight: 700; line-height: 1.18;
162 margin: 0 0 var(--sp-3); letter-spacing: -0.025em; text-wrap: balance; }
163h2 { font-size: var(--fs-4); font-weight: 700; line-height: 1.3;
164 margin: var(--sp-5) 0 var(--sp-3); letter-spacing: -0.015em; }
123165 h3 { font-size: var(--fs-3); font-weight: 600; margin: var(--sp-5) 0 var(--sp-2); }
124166
125167 code, pre, .code, td.mode, td.size {
@@ -127,167 +169,213 @@ code, pre, .code, td.mode, td.size {
127169 font-size: var(--fs-1);
128170 }
129171
130/* layout: one shared container aligns header, content, and footer */
131.container {
132 max-width: 64rem;
133 margin: 0 auto;
134 padding: 0 var(--sp-5);
135}
136/* the shell: an ink bar in both schemes, Carbon UI-shell style. Scoped
137 to the page header so cards with their own <header> stay untouched. */
138body > header {
172/* ---- shell: black rail, content pane ---- */
173.shell { display: flex; align-items: stretch; min-height: 100vh; }
174
175.rail {
176 flex: none;
177 width: 13rem;
139178 background: var(--shell-bg);
140 border-bottom: 1px solid var(--shell-line);
141 padding: 0;
179 color: var(--shell-fg);
180 border-right: 1px solid var(--shell-line);
142181 position: sticky;
143182 top: 0;
144 z-index: 30;
183 height: 100vh;
184 display: flex;
185 flex-direction: column;
145186 }
146a.site {
147 font-weight: 600;
148 color: var(--shell-fg);
149 letter-spacing: -0.01em;
150 display: inline-flex;
187a.brand {
188 display: flex;
151189 align-items: center;
152190 gap: var(--sp-2);
153 padding: 0 var(--sp-4);
191 padding: var(--sp-3) var(--sp-4);
192 color: var(--shell-fg);
193 font-weight: 700;
194 font-size: var(--fs-3);
195 letter-spacing: -0.02em;
196 border-bottom: 1px solid var(--shell-line);
154197 }
155a.site:hover { text-decoration: none; color: #ffffff; }
156a.site svg.mark { display: block; }
157a.site svg.mark rect { fill: #0f62fe; }
158a.site svg.mark path { stroke: #ffffff; }
159nav.topnav {
160 display: flex;
161 align-items: center;
162 gap: 0;
163 height: 3rem;
164}
165nav.topnav .spacer { flex: 1; }
166/* the avatar square is the only nav item in the shell; everything else
167 lives in the rail */
168a.avatar {
169 width: 2rem;
170 height: 2rem;
171 margin-right: var(--sp-3);
172 display: inline-flex;
173 align-items: center;
174 justify-content: center;
175 background: var(--accent);
176 color: #ffffff;
198a.brand:hover { text-decoration: none; color: #ffffff; }
199a.brand svg.mark { display: block; flex: none; }
200a.brand span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
201
202.railbody { flex: 1; overflow-y: auto; padding: var(--sp-3) 0; }
203ul.raillist { list-style: none; margin: 0; padding: 0; }
204ul.raillist a {
205 display: block;
206 padding: 0.28rem var(--sp-4);
207 color: var(--shell-fg);
177208 font-size: var(--fs-2);
209 border-left: 3px solid transparent;
210 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
211}
212ul.raillist a:hover { background: var(--shell-hover); text-decoration: none; }
213/* orange marks position — the rail's only accent */
214ul.raillist a[aria-current] {
215 border-left-color: var(--shell-mark);
216 background: var(--shell-hover);
178217 font-weight: 600;
179 flex: none;
180218 }
181a.avatar:hover { text-decoration: none; filter: brightness(1.12); }
182a.avatar.guest { background: #393939; color: #f4f4f4; }
219ul.raillist .owner { color: var(--shell-muted); }
220ul.raillist.wide a { white-space: normal; padding-top: var(--sp-1); padding-bottom: var(--sp-1); }
221ul.raillist.wide b { display: block; font-weight: 500; font-size: var(--fs-1); }
222ul.raillist.wide .owner { font-size: var(--fs-0); }
183223
184/* the rail: persistent navigation, global links first, repo sections
185 below them, the signed-in user pinned to the bottom */
186.shell { display: flex; flex: 1; align-items: stretch; }
187.siderail {
188 flex: none;
189 width: 13.5rem;
190 background: var(--surface);
191 border-right: 1px solid var(--line);
192 position: sticky;
193 top: 3rem;
194 height: calc(100vh - 3rem);
224.raillabel {
195225 display: flex;
196 flex-direction: column;
226 align-items: center;
227 gap: var(--sp-2);
228 margin: var(--sp-4) 0 var(--sp-1);
229 padding: 0 var(--sp-4);
230 font-family: var(--mono);
231 font-size: var(--fs-0);
232 font-weight: 500;
233 letter-spacing: 0.1em;
234 text-transform: uppercase;
235 color: var(--shell-muted);
236}
237.raillabel .count {
238 font-family: var(--sans);
239 font-size: var(--fs-0);
240 font-weight: 700;
241 letter-spacing: 0;
242 background: var(--shell-mark);
243 color: #000000;
244 padding: 0 0.35rem;
245 border-radius: var(--r-sm);
197246 }
198.railmain { flex: 1; overflow-y: auto; padding: var(--sp-3) 0; }
199247 .railfoot {
200248 flex: none;
201 border-top: 1px solid var(--line);
202 padding: var(--sp-2) 0;
203 background: var(--surface);
204}
205.siderail a, .siderail button.linklike {
249 border-top: 1px solid var(--shell-line);
250 padding: var(--sp-2) var(--sp-4);
206251 display: flex;
207252 align-items: center;
208 gap: var(--sp-2);
209 width: 100%;
210 text-align: left;
211 color: var(--fg);
253 gap: var(--sp-3);
212254 font-size: var(--fs-2);
213 padding: 0.4rem var(--sp-4);
214 border-left: 3px solid transparent;
215 line-height: 1.35;
216255 }
217.siderail a:hover, .siderail button.linklike:hover {
218 background: var(--hover);
219 text-decoration: none;
220 color: var(--fg);
256.railfoot a { color: var(--shell-fg); }
257a.railuser { display: inline-flex; align-items: center; gap: var(--sp-2); }
258.railfoot .linklike {
259 background: none; border: 0; padding: 0; cursor: pointer;
260 color: var(--shell-muted); font: inherit; text-decoration: underline;
261}
262.railfoot .linklike:hover { color: var(--shell-fg); }
263.avatar {
264 width: 1.4rem; height: 1.4rem; flex: none;
265 display: inline-grid; place-items: center;
266 background: var(--fill);
267 color: #ffffff;
268 font-size: var(--fs-0);
269 font-weight: 700;
270 border-radius: var(--r-sm);
221271 }
222.siderail a.active {
223 border-left-color: var(--accent);
224 background: var(--hover);
272
273.pane { flex: 1; min-width: 0; display: flex; flex-direction: column; }
274.content {
275 flex: 1;
276 padding: var(--sp-5) var(--sp-5) var(--sp-6);
277 max-width: 78rem;
278 width: 100%;
279}
280
281/* ---- repo header: identity and tabs on every page, the rest once ---- */
282.repohead {
283 padding: var(--sp-4) var(--sp-5) 0;
284 border-bottom: 1px solid var(--line);
285}
286.repohead .identity { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
287.repohead .identity .grow { flex: 1; }
288.repotitle {
289 margin: 0;
290 font-size: var(--fs-4);
291 font-weight: 700;
292 letter-spacing: -0.02em;
293 line-height: 1.3;
294}
295h1.repotitle { font-size: var(--fs-5); }
296.repotitle a { color: var(--fg); }
297.repotitle a.owner { color: var(--muted); font-weight: 400; }
298.repotitle a:hover { color: var(--accent); text-decoration: none; }
299.repotitle .sep { color: var(--muted); margin: 0 0.12em; font-weight: 400; }
300.repodesc { margin: var(--sp-2) 0 0; color: var(--muted); font-size: var(--fs-2); max-width: 62ch; }
301.repometa { margin: var(--sp-1) 0 0; color: var(--muted); font-size: var(--fs-1); }
302.repohead .topics { margin: var(--sp-2) 0 0; }
303
304button.btn {
305 font: inherit;
306 font-size: var(--fs-1);
225307 font-weight: 600;
308 padding: 0.15rem 0.7rem;
309 border: 1px solid var(--line);
310 border-radius: var(--r-sm);
311 background: var(--bg);
312 color: var(--fg);
313 cursor: pointer;
226314 }
227.siderail .glabel {
228 display: block;
229 font-size: var(--fs-0);
315button.btn:hover { border-color: var(--accent); color: var(--accent); }
316button.btn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
317form.inline { display: inline; }
318
319nav.tabs {
320 display: flex;
321 gap: var(--sp-1);
322 margin-top: var(--sp-4);
323 overflow-x: auto;
324 scrollbar-width: none;
325}
326nav.tabs::-webkit-scrollbar { display: none; }
327nav.tabs a {
328 display: inline-flex;
329 align-items: center;
330 gap: var(--sp-2);
331 padding: var(--sp-2) var(--sp-3) 0.6rem;
230332 color: var(--muted);
231 letter-spacing: 0.04em;
232 padding: 0 var(--sp-4);
233 margin: var(--sp-4) 0 var(--sp-1);
333 font-size: var(--fs-2);
334 white-space: nowrap;
335 border-bottom: 2px solid transparent;
336 margin-bottom: -1px;
234337 }
235.siderail .glabel:not(.sub) {
338nav.tabs a:hover { color: var(--fg); text-decoration: none; }
339/* orange marks position here too */
340nav.tabs a[aria-current] {
236341 color: var(--fg);
237 font-weight: 600;
238 border-top: 1px solid var(--line);
239 padding-top: var(--sp-3);
240 overflow-wrap: anywhere;
342 font-weight: 700;
343 border-bottom-color: var(--mark);
241344 }
242.siderail .glabel.sub { margin-top: var(--sp-2); }
243.siderail .railuser { font-weight: 600; }
244.siderail .avatar.sm {
245 width: 1.25rem;
246 height: 1.25rem;
247 margin: 0;
345nav.tabs a i {
346 font-style: normal;
347 font-family: var(--mono);
248348 font-size: var(--fs-0);
249 background: var(--accent);
250 color: #ffffff;
251}
252main.container {
253 width: 100%;
254 min-width: 0;
255 flex: 1;
256 margin: 0;
257 max-width: 66rem;
258 padding-top: var(--sp-4);
259 padding-bottom: var(--sp-6);
349 background: var(--hover);
350 border-radius: var(--r-sm);
351 padding: 0 0.35rem;
260352 }
353
354/* ---- mobile: the rail becomes a single strip, not a wall of links ---- */
261355 @media (max-width: 52rem) {
262 /* the rail folds into a wrapping strip above the content */
263356 .shell { display: block; }
264 .siderail {
357 .rail {
265358 width: auto;
266359 height: auto;
267360 position: static;
268361 flex-direction: row;
269 flex-wrap: wrap;
270362 align-items: center;
271 border-right: none;
272 border-bottom: 1px solid var(--line);
273 }
274 .railmain, .railfoot {
275 display: flex;
276 flex-wrap: wrap;
277 align-items: center;
278 overflow: visible;
279 padding: var(--sp-1) var(--sp-2);
280 border-top: none;
281 }
282 .railmain { flex: 1 1 auto; }
283 .siderail a, .siderail button.linklike {
284 width: auto;
285 border-left: none;
286 border-bottom: 3px solid transparent;
287 padding: var(--sp-1) var(--sp-2);
363 gap: var(--sp-2);
364 border-right: 0;
365 border-bottom: 1px solid var(--shell-line);
366 overflow-x: auto;
288367 }
289 .siderail a.active { border-left: none; border-bottom-color: var(--accent); background: none; }
290 .siderail .glabel { display: none; }
368 a.brand { border-bottom: 0; padding-right: var(--sp-2); }
369 .railbody { flex: 1; overflow: visible; padding: 0; display: flex; align-items: center; }
370 /* pinned repos and the review queue both live on the dashboard, so the
371 strip keeps only what has nowhere else to go */
372 .railgroup { display: none; }
373 ul.raillist { display: flex; }
374 ul.raillist a { border-left: 0; border-bottom: 2px solid transparent; padding: var(--sp-2) var(--sp-3); }
375 ul.raillist a[aria-current] { border-left: 0; border-bottom-color: var(--shell-mark); }
376 .railfoot { border-top: 0; padding-left: var(--sp-2); }
377 .content { padding: var(--sp-4) var(--sp-4) var(--sp-5); }
378 .repohead { padding: var(--sp-3) var(--sp-4) 0; }
291379 }
292380
293381 /* tables */
@@ -301,66 +389,6 @@ td {
301389 td:last-child { padding-right: 0; }
302390 td.size, td.mode { color: var(--muted); white-space: nowrap; }
303391
304/* repo header */
305.repohead { margin-bottom: var(--sp-4); }
306h1.repotitle { margin: var(--sp-4) 0 var(--sp-1); font-weight: 400; }
307h1.repotitle a { color: var(--fg); }
308h1.repotitle a:last-of-type { font-weight: 650; }
309h1.repotitle a:hover { color: var(--accent); text-decoration: none; }
310h1.repotitle .sep { color: var(--muted); margin: 0 0.15em; }
311form.pinform { display: inline; margin-left: var(--sp-2); vertical-align: middle; }
312button.pinbtn {
313 background: none;
314 border: 1px solid var(--line);
315 border-radius: var(--r-pill);
316 color: var(--muted);
317 font-size: var(--fs-0);
318 padding: 0.05rem 0.55rem;
319 cursor: pointer;
320}
321button.pinbtn:hover { border-color: var(--accent); color: var(--accent); filter: none; }
322button.pinbtn.pinned { color: var(--accent); border-color: var(--accent); }
323a.chip.label:hover, a.chip.topic:hover { text-decoration: none; filter: brightness(1.15); }
324.repohead .desc { margin: 0 0 var(--sp-2); }
325
326/* repo rail: grouped vertical navigation on the left */
327nav.tabs { display: flex; flex-direction: column; margin-top: var(--sp-1); }
328nav.tabs .glabel {
329 font-size: var(--fs-0);
330 color: var(--muted);
331 letter-spacing: 0.04em;
332 margin: var(--sp-4) 0 var(--sp-1);
333}
334nav.tabs .glabel:first-child { margin-top: 0; }
335nav.tabs a {
336 color: var(--fg);
337 padding: 0.32rem var(--sp-3);
338 font-size: var(--fs-2);
339 border-left: 3px solid transparent;
340}
341nav.tabs a:hover { background: var(--hover); text-decoration: none; }
342nav.tabs a.active {
343 border-left-color: var(--accent);
344 background: var(--surface);
345 font-weight: 600;
346}
347@media (max-width: 52rem) {
348 /* the rail folds back into a wrapping strip on narrow screens */
349 main.container:has(> nav.tabs) { display: block; }
350 nav.tabs { flex-direction: row; flex-wrap: wrap; gap: 0 var(--sp-1); border-bottom: 1px solid var(--line); padding-bottom: var(--sp-2); position: static; }
351 nav.tabs .glabel { display: none; }
352 nav.tabs a { border-left: none; border-bottom: 3px solid transparent; padding: var(--sp-1) var(--sp-2); }
353 nav.tabs a.active { border-bottom-color: var(--accent); background: none; }
354}
355
356.clone { color: var(--muted); font-size: var(--fs-2); }
357.clone code {
358 background: var(--code-bg);
359 border: 1px solid var(--line);
360 padding: 0.15rem 0.5rem;
361 border-radius: var(--r-sm);
362}
363
364392 /* path bar on tree and blob pages */
365393 .pathbar {
366394 display: flex;
internal/web/templates/blame.html +1 −1
@@ -1,6 +1,6 @@
11 {{define "title"}}blame: {{.Path}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
3<h1 class="vh">Blame: {{.Path}}</h1>
44 <div class="pathbar">
55 <span class="refchip">{{template "branchicon"}} {{.Ref}}</span>
66 <span class="crumbs"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a>/{{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}<strong>{{.Base}}</strong></span>
internal/web/templates/blob.html +1 −1
@@ -1,6 +1,6 @@
11 {{define "title"}}{{.Path}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
3<h1 class="vh">{{.Path}}</h1>
44 <div class="pathbar">
55 {{template "refmenu" .}}
66 <span class="crumbs"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a>/{{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}<strong>{{.Base}}</strong></span>
internal/web/templates/build.html +1 −2
@@ -1,8 +1,7 @@
11 {{define "title"}}build {{.Build.Number}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
43 <div class="headrow">
5<h1>build {{.Build.Number}} <span class="chip check-{{.Build.Status}}">{{.Build.Status}}</span></h1>
4<h1>Build {{.Build.Number}} <span class="chip check-{{.Build.Status}}">{{.Build.Status}}</span></h1>
65 </div>
76 <p class="meta">{{.Build.Job}} on {{.Build.Ref}} · <code><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/commit/{{.Build.SHA}}">{{printf "%.10s" .Build.SHA}}</a></code> · queued {{when .Build.CreatedAt}}{{if .Build.FinishedAt}} · finished {{when .Build.FinishedAt}}{{end}}</p>
87 {{if .Log}}<pre class="code buildlog">{{.Log}}</pre>{{else}}<p class="empty-note">no log yet</p>{{end}}
internal/web/templates/builds.html +1 −1
@@ -1,6 +1,6 @@
11 {{define "title"}}builds · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
3<h1>Builds</h1>
44 <ul class="loglist">
55 {{range .Builds}}<li>
66 <div class="commitmain">
internal/web/templates/commit.html +1 −2
@@ -1,8 +1,7 @@
11 {{define "title"}}{{.ShortSHA}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
43 <div class="commithead">
5 <h2>commit <code>{{.ShortSHA}}</code></h2>
4 <h1>Commit <code>{{.ShortSHA}}</code></h1>
65 <p class="meta"><code class="fullsha">{{.SHA}}</code></p>
76 {{if .Parents}}<p class="meta">parent{{if gt (len .Parents) 1}}s{{end}}:{{range .Parents}} <code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.}}">{{short .}}</a></code>{{end}}</p>{{end}}
87 <p>{{template "sigbadge" .Sig}}{{range .Checks}} <span class="badge check-{{.State}}">{{.Context}}: {{.State}}</span>{{end}}</p>
internal/web/templates/issue.html +1 −2
@@ -1,7 +1,6 @@
11 {{define "title"}}#{{.Issue.Number}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
4<h2 class="issuetitle">{{.Issue.Title}} <span class="issuenumber">#{{.Issue.Number}}</span></h2>
3<h1 class="issuetitle">{{.Issue.Title}} <span class="issuenumber">#{{.Issue.Number}}</span></h1>
54 <p class="issuemeta"><span class="chip {{if eq .Issue.State "open"}}chip-open{{else}}chip-done{{end}}">{{.Issue.State}}</span>
65 <a href="/{{.Issue.Author}}">{{.Issue.Author}}</a> opened this on {{when .Issue.CreatedAt}}
76 {{if .Issue.Labels}} · {{range .Issue.Labels}}<a class="chip label" style="{{index $.LabelColors .}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/issues?label={{.}}">{{.}}</a> {{end}}{{end}}
internal/web/templates/issuenew.html +1 −2
@@ -1,7 +1,6 @@
11 {{define "title"}}new issue · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
4<h2>new issue</h2>
3<h1>New issue</h1>
54 {{if gt (len .Templates) 1}}<p class="meta">template:
65 {{range .Templates}}{{if eq .Name $.Template}}<strong>{{.Name}}</strong>{{else}}<a href="?template={{.Name}}">{{.Name}}</a>{{end}} {{end}}</p>{{end}}
76 <form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues/new" class="commentform">
internal/web/templates/issues.html +1 −2
@@ -1,8 +1,7 @@
11 {{define "title"}}issues · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
43 <div class="listhead">
5 <h2>issues</h2>
4 <h1>Issues</h1>
65 <nav class="filters">
76 <a {{if eq .State "open"}}class="active" {{end}}href="?state=open">open</a>
87 <a {{if eq .State "closed"}}class="active" {{end}}href="?state=closed">closed</a>
internal/web/templates/layout.html +70 −45
@@ -8,65 +8,90 @@
88 <link rel="icon" href="/favicon.svg" type="image/svg+xml">
99 </head>
1010 <body>
11<header>
12 <nav class="topnav">
13 <a class="site" href="/"><svg class="mark" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><rect x="1" y="1" width="22" height="22" rx="6" fill="var(--accent)"/><path d="M5 9.5c2-2.5 4-2.5 6 0s4 2.5 6 0" stroke="var(--accent-fg)" stroke-width="2.2" fill="none" stroke-linecap="round"/><path d="M5 15c2-2.5 4-2.5 6 0s4 2.5 6 0" stroke="var(--accent-fg)" stroke-width="2.2" fill="none" stroke-linecap="round"/></svg>{{.Site}}</a>
14 <span class="spacer"></span>
15 {{if .Viewer}}<a class="avatar" href="/{{.Viewer}}" title="{{.Viewer}}" aria-label="{{.Viewer}}">{{initial .Viewer}}</a>
16 {{else}}<a class="avatar guest" href="/login" title="sign in" aria-label="sign in"></a>{{end}}
17 </nav>
18</header>
11<a class="skip" href="#content">Skip to content</a>
1912 <div class="shell">
20<nav class="siderail" aria-label="site">
21 <div class="railmain">
22 <a {{if eq (str . "Tab") "explore"}}class="active" {{end}}href="/explore">explore</a>
23 {{if .Viewer}}<a href="/new">new repository</a>{{end}}
24 {{with field . "Repo"}}
25 <span class="glabel">{{.OwnerName}}/{{.Name}}</span>
26 <span class="glabel sub">code</span>
27 <a {{if eq (str $ "Tab") "files"}}class="active" {{end}}href="/{{.OwnerName}}/{{.Name}}">files</a>
28 <a {{if eq (str $ "Tab") "log"}}class="active" {{end}}href="/{{.OwnerName}}/{{.Name}}/log">log</a>
29 <a {{if eq (str $ "Tab") "refs"}}class="active" {{end}}href="/{{.OwnerName}}/{{.Name}}/refs">refs</a>
30 <span class="glabel sub">work</span>
31 <a {{if eq (str $ "Tab") "issues"}}class="active" {{end}}href="/{{.OwnerName}}/{{.Name}}/issues">issues</a>
32 <a {{if eq (str $ "Tab") "merge requests"}}class="active" {{end}}href="/{{.OwnerName}}/{{.Name}}/mrs">merge requests</a>
33 <a {{if eq (str $ "Tab") "builds"}}class="active" {{end}}href="/{{.OwnerName}}/{{.Name}}/builds">builds</a>
34 <span class="glabel sub">publish</span>
35 <a {{if eq (str $ "Tab") "releases"}}class="active" {{end}}href="/{{.OwnerName}}/{{.Name}}/releases">releases</a>
36 {{if field $ "HasWiki"}}<a {{if eq (str $ "Tab") "wiki"}}class="active" {{end}}href="/{{.OwnerName}}/{{.Name}}/wiki">wiki</a>{{end}}
37 <span class="glabel sub">tools</span>
38 <a {{if eq (str $ "Tab") "search"}}class="active" {{end}}href="/{{.OwnerName}}/{{.Name}}/search">search</a>
39 <a href="/{{.OwnerName}}/{{.Name}}/archive/{{str $ "Ref"}}.tar.gz">archive</a>
13
14{{$here := ""}}{{with field . "Repo"}}{{$here = printf "%s/%s" .OwnerName .Name}}{{end}}
15<nav class="rail" aria-label="Site">
16 <a class="brand" href="/">{{template "mark"}}<span>{{.Site}}</span></a>
17 <div class="railbody">
18 <ul class="raillist">
19 {{if .Viewer}}<li><a href="/">Dashboard</a></li>{{end}}
20 <li><a {{if eq (str . "Tab") "explore"}}aria-current="page" {{end}}href="/explore">Explore</a></li>
21 {{if .Viewer}}<li><a href="/new">New repository</a></li>{{end}}
22 </ul>
23 {{with .Rail.Pinned}}
24 <div class="railgroup">
25 <p class="raillabel" id="rail-pinned">Pinned</p>
26 <ul class="raillist" aria-labelledby="rail-pinned">
27 {{range .}}<li><a {{if eq $here (printf "%s/%s" .Owner .Name)}}aria-current="page" {{end}}href="/{{.Owner}}/{{.Name}}"><span class="owner">{{.Owner}}/</span>{{.Name}}</a></li>
28 {{end}}
29 </ul>
30 </div>
31 {{end}}
32 {{with .Rail.Reviews}}
33 <div class="railgroup">
34 <p class="raillabel" id="rail-reviews">Waiting on you <b class="count">{{len .}}</b></p>
35 <ul class="raillist wide" aria-labelledby="rail-reviews">
36 {{range .}}<li><a href="/{{.RepoPath}}/mrs/{{.Number}}"><b>!{{.Number}} {{.Title}}</b><span class="owner">{{.RepoPath}}</span></a></li>
37 {{end}}
38 </ul>
39 </div>
4040 {{end}}
4141 </div>
4242 <div class="railfoot">
43 {{if .Viewer}}<a class="railuser" href="/{{.Viewer}}"><span class="avatar sm">{{initial .Viewer}}</span>{{.Viewer}}</a>
44 <form method="post" action="/logout"><button type="submit" class="linklike">log out</button></form>
45 {{else}}<a href="/login">sign in</a>{{end}}
43 {{if .Viewer}}<a class="railuser" href="/{{.Viewer}}"><span class="avatar">{{initial .Viewer}}</span>{{.Viewer}}</a>
44 <form method="post" action="/logout"><button type="submit" class="linklike">Log out</button></form>
45 {{else}}<a href="/login">Sign in</a>{{end}}
4646 </div>
4747 </nav>
48<main class="container">
48
49<div class="pane">
50{{with field . "Repo"}}
51<header class="repohead">
52 <div class="identity">
53 {{if field $ "RepoHome"}}<h1 class="repotitle"><a class="owner" href="/{{.OwnerName}}">{{.OwnerName}}</a><span class="sep">/</span>{{.Name}}</h1>
54 {{else}}<p class="repotitle"><a class="owner" href="/{{.OwnerName}}">{{.OwnerName}}</a><span class="sep">/</span><a href="/{{.OwnerName}}/{{.Name}}">{{.Name}}</a></p>{{end}}
55 {{if eq .Visibility "private"}}<span class="chip">Private</span>{{end}}
56 {{if .Settings.Archived}}<span class="chip">Archived</span>{{end}}
57 <span class="grow"></span>
58 {{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}}">{{if field $ "Pinned"}}★ Pinned{{else}}☆ Pin{{end}}</button></form>{{end}}
59 </div>
60 {{if field $ "RepoHome"}}
61 {{with field $ "Desc"}}<p class="repodesc">{{.}}</p>{{end}}
62 {{with field $ "Topics"}}<p class="topics">{{range .}}<a class="chip topic" href="/explore?q={{.}}">{{.}}</a> {{end}}</p>{{end}}
63 {{with $.Repo.Settings.Website}}<p class="repometa"><a href="{{.}}" rel="nofollow">{{.}}</a></p>{{end}}
64 {{range field $ "Mirrors"}}<p class="repometa">{{if eq .Direction "push"}}Mirrors to{{else}}Mirrors from{{end}} <a href="{{.URL}}" rel="nofollow">{{.Target}}</a>{{if .Error}} · <span class="bad">sync error: {{.Error}}</span>{{else if .Synced}} · synced {{.Synced}}{{end}}</p>
65 {{end}}
66 {{end}}
67 <nav class="tabs" aria-label="Repository">
68 {{$top := topTab (str $ "Tab")}}
69 <a {{if eq $top "code"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}">Code</a>
70 <a {{if eq $top "issues"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}/issues">Issues{{with field $ "OpenIssues"}}{{if .}} <i>{{.}}</i>{{end}}{{end}}</a>
71 <a {{if eq $top "mrs"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}/mrs">Merge requests{{with field $ "OpenMRs"}}{{if .}} <i>{{.}}</i>{{end}}{{end}}</a>
72 <a {{if eq $top "builds"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}/builds">Builds</a>
73 <a {{if eq $top "releases"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}/releases">Releases</a>
74 {{if field $ "HasWiki"}}<a {{if eq $top "wiki"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}/wiki">Wiki</a>{{end}}
75 </nav>
76</header>
77{{end}}
78
79<main id="content" class="content">
4980 {{template "content" .}}
5081 </main>
51</div>
82
5283 <footer>
53 <div class="container"><p>powered by <a href="https://gitbay.org/krz/gitbay">gitbay</a>{{with gitbayVersion}} · <code><a href="https://gitbay.org/krz/gitbay/commit/{{gitbayCommit}}">{{.}}</a></code>{{end}} · <a href="/privacy">privacy</a></p></div>
84 <p>Powered by <a href="https://gitbay.org/krz/gitbay">gitbay</a>{{with gitbayVersion}} · <code><a href="https://gitbay.org/krz/gitbay/commit/{{gitbayCommit}}">{{.}}</a></code>{{end}} · <a href="/privacy">Privacy</a></p>
5485 </footer>
86</div>
87</div>
5588 </body>
5689 </html>{{end}}
5790
58{{define "repoheader"}}
59<div class="repohead">
60<h1 class="repotitle"><a class="owner" href="/{{.Repo.OwnerName}}">{{.Repo.OwnerName}}</a><span class="sep">/</span><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a>{{if eq .Repo.Visibility "private"}} <span class="chip chip-neutral">private</span>{{end}}{{if .Repo.Settings.Archived}} <span class="chip chip-stale">archived</span>{{end}}{{if .Viewer}}<form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/pin" class="inline pinform"><button type="submit" class="pinbtn{{if .Pinned}} pinned{{end}}" title="{{if .Pinned}}unpin from dashboard{{else}}pin to dashboard{{end}}">{{if .Pinned}}★ pinned{{else}}☆ pin{{end}}</button></form>{{end}}</h1>
61{{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}}
62{{if .Repo.Settings.Website}}<p class="meta repowebsite"><a href="{{.Repo.Settings.Website}}" rel="nofollow">{{.Repo.Settings.Website}}</a></p>{{end}}
63{{if .Topics}}<p class="topics">{{range .Topics}}<span class="chip topic">{{.}}</span> {{end}}</p>{{end}}
64{{range .Mirrors}}<p class="meta mirrorline">{{if eq .Direction "push"}}mirrors to{{else}}mirrors from{{end}} <a href="{{.URL}}" rel="nofollow">{{.Target}}</a>{{if .Error}} · <span class="mirrorerr">sync error: {{.Error}}</span>{{else if .Synced}} · synced {{.Synced}}{{end}}</p>
65{{end}}</div>
66{{end}}
91{{define "mark"}}<svg class="mark" width="19" height="19" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.25 18.75 12H5.25z" fill="#ff6b3d"/><rect x="3" y="13.5" width="18" height="3" fill="currentColor"/><rect x="7.5" y="18" width="9" height="3" fill="currentColor"/></svg>{{end}}
6792
6893 {{define "reporow"}}<li>
69 <p class="reponame"><a href="/{{.OwnerName}}/{{.Name}}">{{.OwnerName}}<span class="sep">/</span><strong>{{.Name}}</strong></a>{{if eq .Visibility "private"}} <span class="chip chip-neutral">private</span>{{end}}{{if .Settings.Archived}} <span class="chip chip-stale">archived</span>{{end}}</p>
94 <p class="reponame"><a href="/{{.OwnerName}}/{{.Name}}">{{.OwnerName}}<span class="sep">/</span><strong>{{.Name}}</strong></a>{{if eq .Visibility "private"}} <span class="chip">Private</span>{{end}}{{if .Settings.Archived}} <span class="chip">Archived</span>{{end}}</p>
7095 {{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}}
7196 {{if .Topics}}<p class="topics">{{range .Topics}}<a class="chip topic" href="/explore?q={{.}}">{{.}}</a> {{end}}</p>{{end}}
7297 <p class="meta">{{template "branchicon"}} {{.DefaultBranch}}{{if .License}} · {{.License}}{{end}}{{if .Updated}} · updated {{.Updated}}{{end}}</p>
@@ -76,7 +101,7 @@
76101 <summary class="refchip">{{template "branchicon"}} {{.Ref}}</summary>
77102 <div class="refdrop">
78103 {{range .Branches}}<a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/{{$.RefKind}}/{{.Name}}/{{$.DirPath}}">{{.Name}}</a>
79 {{end}}<a class="allrefs" href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/refs">all refs →</a>
104 {{end}}<a class="allrefs" href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/refs">All refs →</a>
80105 </div>
81106 </details>{{else}}<span class="refchip">{{template "branchicon"}} {{.Ref}}</span>{{end}}{{end}}
82107
internal/web/templates/log.html +1 −1
@@ -1,7 +1,7 @@
11 {{define "title"}}log · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
43 {{if .FilePath}}<p class="meta">history of <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/blob/{{.Ref}}/{{.FilePath}}"><code>{{.FilePath}}</code></a> · <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log">full log</a></p>{{end}}
4<h1 class="vh">Commits</h1>
55 <ul class="loglist">
66 {{range .Commits}}<li>
77 <div class="commitmain">
internal/web/templates/milestones.html +1 −2
@@ -1,8 +1,7 @@
11 {{define "title"}}milestones · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
43 <div class="listhead">
5 <h2>milestones</h2>
4 <h1>Milestones</h1>
65 <nav class="filters">
76 <a {{if eq .State "open"}}class="active" {{end}}href="?state=open">open</a>
87 <a {{if eq .State "closed"}}class="active" {{end}}href="?state=closed">closed</a>
internal/web/templates/mr.html +1 −2
@@ -1,7 +1,6 @@
11 {{define "title"}}!{{.MR.Number}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
4<h2 class="issuetitle">{{.MR.Title}} <span class="issuenumber">!{{.MR.Number}}</span></h2>
3<h1 class="issuetitle">{{.MR.Title}} <span class="issuenumber">!{{.MR.Number}}</span></h1>
54 <p class="issuemeta"><span class="chip chip-{{.MR.State}}">{{.MR.State}}</span>
65 <a href="/{{.MR.Author}}">{{.MR.Author}}</a> wants to merge {{if .MR.SourcePath}}{{.MR.SourcePath}}:{{end}}{{.MR.SourceRef}} into {{.MR.TargetRef}}
76 at <code>{{short .MR.HeadSHA}}</code>{{if .MR.Milestone}} · milestone <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/milestones">{{.MR.Milestone}}</a>{{end}}</p>
internal/web/templates/mrs.html +1 −2
@@ -1,8 +1,7 @@
11 {{define "title"}}merge requests · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
43 <div class="listhead">
5 <h2>merge requests</h2>
4 <h1>Merge requests</h1>
65 <nav class="filters">
76 <a {{if eq .State "open"}}class="active" {{end}}href="?state=open">open</a>
87 <a {{if eq .State "merged"}}class="active" {{end}}href="?state=merged">merged</a>
internal/web/templates/refs.html +3 −3
@@ -1,8 +1,8 @@
11 {{define "title"}}refs · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
4<h2>branches</h2>
3<h1>Refs</h1>
4<h2>Branches</h2>
55 <table class="refs">{{range .Branches}}<tr><td class="name">{{template "branchicon"}} <a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/tree/{{.Name}}/">{{.Name}}</a></td><td class="sha"><code>{{short .SHA}}</code></td></tr>{{else}}<tr><td class="empty">none</td></tr>{{end}}</table>
6<h2>tags</h2>
6<h2>Tags</h2>
77 <table class="refs">{{range .Tags}}<tr><td class="name"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/tree/{{.Name}}/">{{.Name}}</a></td><td class="sha"><code>{{short .SHA}}</code></td></tr>{{else}}<tr><td class="empty">none</td></tr>{{end}}</table>
88 {{end}}
internal/web/templates/releases.html +1 −2
@@ -1,7 +1,6 @@
11 {{define "title"}}releases · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
4<h2>releases</h2>
3<h1>Releases</h1>
54 {{range $rel := .Releases}}
65 <article class="release">
76 <header class="releasehead">
internal/web/templates/search.html +1 −1
@@ -1,6 +1,6 @@
11 {{define "title"}}search · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
3<h1>Search</h1>
44 <form method="get" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/search" class="searchform">
55 <input type="search" name="q" value="{{.Query}}" placeholder="search file contents on {{.Ref}}" autofocus>
66 <button type="submit">search</button>
internal/web/templates/tree.html +1 −1
@@ -1,6 +1,6 @@
11 {{define "title"}}{{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
3{{if .DirPath}}<h1 class="vh">{{.DirPath}}</h1>{{end}}
44 <div class="pathbar">
55 {{template "refmenu" .}}
66 <span class="crumbs"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a>/{{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}</span>
internal/web/templates/wiki.html +1 −1
@@ -1,6 +1,6 @@
11 {{define "title"}}wiki{{if .Page}}: {{.Page}}{{end}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{template "repoheader" .}}
3<h1 class="vh">Wiki{{if .Page}}: {{.Page}}{{end}}</h1>
44 {{if .Missing}}<p class="empty-note">no wiki yet — create one by pushing pages:<br>
55 <code>git clone ssh://git@{{.Host}}/{{.Repo.OwnerName}}/{{.Repo.Name}}.wiki.git</code> then add <code>Home.md</code> (or .org) and push.</p>
66 {{else}}
internal/web/web.go +14
@@ -77,6 +77,20 @@ var funcs = template.FuncMap{
7777 },
7878 "add": func(a, b int) int { return a + b },
7979 "sub": func(a, b int) int { return a - b },
80 // topTab maps a page's Tab to the repo header tab that should read as
81 // current. Everything that browses the tree or its history sits under
82 // code, whose own toolbar carries the log, search, and refs links.
83 "topTab": func(tab string) string {
84 switch tab {
85 case "files", "log", "refs", "search":
86 return "code"
87 case "issues":
88 return "issues"
89 case "merge requests":
90 return "mrs"
91 }
92 return tab
93 },
8094 // initial is the avatar letter for a username.
8195 "initial": func(s string) string {
8296 for _, r := range s {