A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit 7b5be134cc

7b5be134cce1a087ae1096772cbd6f76716d307f

parent: b4684c088d

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-25T21:57:50Z

Carbon: black shell, left repo rail, sharp lines, IBM Plex

The design-directions winner (#35). Tokens move to a Carbon-school
palette — g10 workspace over white tiles, g100 in dark, one blue —
with every radius zeroed. The header becomes an ink shell in both
schemes; repo pages grow a grouped left rail (code / work / publish /
tools) replacing the ten-tab row, folding back to a strip on narrow
screens via a :has() grid with block-flow fallback. IBM Plex Sans and
Mono are embedded and served same-origin, keeping the CSP free of font
hosts. Ref #35
internal/httpd/routes.go +5
@@ -40,6 +40,11 @@ func (s *Server) Routes() []Route {
4040 Route{Method: "GET", Pattern: "/explore", Handler: s.explore},
4141 Route{Method: "GET", Pattern: "/privacy", Handler: s.privacy},
4242 Route{Method: "GET", Pattern: "/static/style.css", Handler: s.stylesheet},
43 // Literal per-file routes: a {name} wildcard is ambiguous against
44 // /{owner}/{repo}/... patterns in ServeMux precedence.
45 Route{Method: "GET", Pattern: "/static/fonts/plex-sans.woff2", Handler: s.font},
46 Route{Method: "GET", Pattern: "/static/fonts/plex-mono-400.woff2", Handler: s.font},
47 Route{Method: "GET", Pattern: "/static/fonts/plex-mono-500.woff2", Handler: s.font},
4348 Route{Method: "GET", Pattern: "/favicon.svg", Handler: s.favicon},
4449 Route{Method: "GET", Pattern: "/{owner}", Handler: s.ownerPage},
4550 Route{Method: "GET", Pattern: "/{owner}/{repo}", Handler: s.repoHome},
internal/httpd/web.go +13
@@ -62,6 +62,19 @@ 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.
67func (s *Server) font(w http.ResponseWriter, r *http.Request) {
68 data, err := web.FontFS.ReadFile("static" + r.URL.Path[len("/static"):])
69 if err != nil {
70 http.NotFound(w, r)
71 return
72 }
73 w.Header().Set("Content-Type", "font/woff2")
74 w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
75 w.Write(data)
76}
77
6578 // notFound renders the designed 404 page with a 404 status. Falls back to
6679 // the stock plain-text response if the template fails.
6780 func (s *Server) notFound(w http.ResponseWriter, r *http.Request) {
internal/web/static/fonts/plex-mono-400.woff2 added

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

internal/web/static/style.css +157 −74
@@ -1,32 +1,62 @@
1/* gitbay stylesheet. One file, light and dark via prefers-color-scheme.
2 Tokens first; components use tokens only. No external requests. */
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. */
5
6@font-face {
7 font-family: "IBM Plex Sans";
8 font-style: normal;
9 font-weight: 400 600;
10 font-display: swap;
11 src: url(/static/fonts/plex-sans.woff2) format("woff2");
12}
13@font-face {
14 font-family: "IBM Plex Mono";
15 font-style: normal;
16 font-weight: 400;
17 font-display: swap;
18 src: url(/static/fonts/plex-mono-400.woff2) format("woff2");
19}
20@font-face {
21 font-family: "IBM Plex Mono";
22 font-style: normal;
23 font-weight: 500;
24 font-display: swap;
25 src: url(/static/fonts/plex-mono-500.woff2) format("woff2");
26}
327
428 :root {
529 /* declare native light+dark support: correct form controls and
630 scrollbars per scheme, and opt out of browser auto-darkening */
731 color-scheme: light dark;
832
9 /* palette */
10 --bg: #ffffff;
11 --fg: #1f2328;
12 --muted: #667079;
13 --faint: #eceff1; /* hairlines, row separators */
14 --line: #d7dde2; /* control borders, card edges */
15 --surface: #f6f8fa; /* cards, code, table hover */
16 --accent: #0000f0; /* blue: links, active tab, primary */
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 */
1742 --accent-fg: #ffffff; /* text on accent */
18 --ok: #1a7f37;
19 --warn: #9a6700;
20 --bad: #cf222e;
21 --done: #8250df; /* merged */
22 --neutral: #667079;
23 --code-bg: #f6f8fa;
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;
2454
2555 /* type */
26 --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
27 "Helvetica Neue", Arial, sans-serif;
28 --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas,
29 "Liberation Mono", monospace;
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;
3060 --fs-0: 0.8rem; /* 12.8px: chips, fine print */
3161 --fs-1: 0.866rem; /* ~13px: metadata, mono in tables */
3262 --fs-2: 0.933rem; /* 14px: dense table text */
@@ -42,28 +72,31 @@
4272 --sp-5: 1.5rem;
4373 --sp-6: 2.5rem;
4474
45 /* radii */
46 --r-sm: 4px;
47 --r-md: 6px;
48 --r-lg: 8px;
75 /* radii: sharp everywhere; only tags stay pills */
76 --r-sm: 0;
77 --r-md: 0;
78 --r-lg: 0;
4979 --r-pill: 999px;
5080 }
5181 @media (prefers-color-scheme: dark) {
5282 :root {
53 --bg: #0d1117;
54 --fg: #e6edf3;
55 --muted: #8d96a0;
56 --faint: #1d242c;
57 --line: #30363d;
58 --surface: #161b22;
59 --accent: #8899ff;
60 --accent-fg: #0a1030;
61 --ok: #3fb950;
62 --warn: #d29922;
63 --bad: #f85149;
64 --done: #a371f7;
65 --neutral: #8d96a0;
66 --code-bg: #161b22;
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: #1e1e1e;
99 --shell-line: #393939;
67100 }
68101 }
69102
@@ -100,36 +133,68 @@ code, pre, .code, td.mode, td.size {
100133 margin: 0 auto;
101134 padding: 0 var(--sp-5);
102135 }
136/* the shell: an ink bar in both schemes, Carbon UI-shell style */
103137 header {
104 border-bottom: 1px solid var(--faint);
105 padding: var(--sp-3) 0;
138 background: var(--shell-bg);
139 border-bottom: 1px solid var(--shell-line);
140 padding: 0;
106141 }
107142 a.site {
108 font-weight: 650;
109 color: var(--fg);
143 font-weight: 600;
144 color: var(--shell-fg);
110145 letter-spacing: -0.01em;
111146 display: inline-flex;
112147 align-items: center;
113148 gap: var(--sp-2);
114149 }
115a.site:hover { text-decoration: none; color: var(--accent); }
150a.site:hover { text-decoration: none; color: #ffffff; }
116151 a.site svg.mark { display: block; }
152a.site svg.mark rect { fill: #0f62fe; }
153a.site svg.mark path { stroke: #ffffff; }
117154 nav.topnav {
118155 display: flex;
119156 align-items: center;
120 gap: var(--sp-4);
157 gap: 0;
158 min-height: 3rem;
121159 }
122160 nav.topnav .spacer { flex: 1; }
123nav.topnav > a:not(.site) { color: var(--fg); font-size: var(--fs-2); }
124nav.topnav > a:not(.site):hover { color: var(--accent); text-decoration: none; }
125nav.topnav a.navuser { font-weight: 600; }
126nav.topnav button.linklike { color: var(--muted); }
127nav.topnav button.linklike:hover { color: var(--accent); text-decoration: none; }
161nav.topnav > a:not(.site) {
162 color: var(--shell-muted);
163 font-size: var(--fs-2);
164 padding: 0.85rem var(--sp-3);
165 line-height: 1;
166}
167nav.topnav > a:not(.site):hover { color: #ffffff; background: #353535; text-decoration: none; }
168nav.topnav a.navuser {
169 font-weight: 600;
170 color: #ffffff;
171 box-shadow: inset 0 -3px 0 #0f62fe;
172}
173nav.topnav button.linklike { color: var(--shell-muted); padding: 0.85rem var(--sp-3); }
174nav.topnav button.linklike:hover { color: #ffffff; background: #353535; text-decoration: none; }
128175 main.container {
129176 width: 100%;
130177 padding-top: var(--sp-3);
131178 padding-bottom: var(--sp-6);
132179 }
180/* repo pages carry a left rail: the repohead spans the top, the sidenav
181 takes column one, everything after flows in column two. Browsers
182 without :has() fall back to single-column flow, which still works. */
183main.container:has(> nav.tabs) {
184 display: grid;
185 grid-template-columns: 11.5rem minmax(0, 1fr);
186 column-gap: var(--sp-6);
187 max-width: 80rem;
188}
189main.container:has(> nav.tabs) > * { grid-column: 2; }
190main.container:has(> nav.tabs) > .repohead { grid-column: 1 / -1; }
191main.container:has(> nav.tabs) > nav.tabs {
192 grid-column: 1;
193 grid-row: 2 / span 99;
194 align-self: start;
195 position: sticky;
196 top: var(--sp-4);
197}
133198
134199 /* tables */
135200 table { border-collapse: collapse; width: 100%; }
@@ -164,24 +229,35 @@ button.pinbtn.pinned { color: var(--accent); border-color: var(--accent); }
164229 a.chip.label:hover, a.chip.topic:hover { text-decoration: none; filter: brightness(1.15); }
165230 .repohead .desc { margin: 0 0 var(--sp-2); }
166231
167/* repo tabs */
168nav.tabs {
169 display: flex;
170 flex-wrap: wrap;
171 gap: var(--sp-1);
172 border-bottom: 1px solid var(--faint);
173 margin-top: var(--sp-3);
232/* repo rail: grouped vertical navigation on the left */
233nav.tabs { display: flex; flex-direction: column; margin-top: var(--sp-1); }
234nav.tabs .glabel {
235 font-size: var(--fs-0);
236 color: var(--muted);
237 letter-spacing: 0.04em;
238 margin: var(--sp-4) 0 var(--sp-1);
174239 }
240nav.tabs .glabel:first-child { margin-top: 0; }
175241 nav.tabs a {
176 color: var(--muted);
177 padding: var(--sp-1) var(--sp-2);
178 border-radius: var(--r-md) var(--r-md) 0 0;
179 border-bottom: 2px solid transparent;
180 margin-bottom: -1px;
242 color: var(--fg);
243 padding: 0.32rem var(--sp-3);
181244 font-size: var(--fs-2);
245 border-left: 3px solid transparent;
246}
247nav.tabs a:hover { background: var(--hover); text-decoration: none; }
248nav.tabs a.active {
249 border-left-color: var(--accent);
250 background: var(--surface);
251 font-weight: 600;
252}
253@media (max-width: 52rem) {
254 /* the rail folds back into a wrapping strip on narrow screens */
255 main.container:has(> nav.tabs) { display: block; }
256 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; }
257 nav.tabs .glabel { display: none; }
258 nav.tabs a { border-left: none; border-bottom: 3px solid transparent; padding: var(--sp-1) var(--sp-2); }
259 nav.tabs a.active { border-bottom-color: var(--accent); background: none; }
182260 }
183nav.tabs a:hover { color: var(--fg); background: var(--surface); text-decoration: none; }
184nav.tabs a.active { color: var(--fg); border-bottom-color: var(--accent); font-weight: 550; }
185261
186262 .clone { color: var(--muted); font-size: var(--fs-2); }
187263 .clone code {
@@ -239,11 +315,12 @@ details.refmenu .refdrop a {
239315 font-family: var(--mono);
240316 font-size: var(--fs-1);
241317 }
242details.refmenu .refdrop a:hover { background: var(--surface); text-decoration: none; }
318details.refmenu .refdrop a:hover { background: var(--hover); text-decoration: none; }
243319 details.refmenu .refdrop a.allrefs { color: var(--accent); font-family: var(--sans); border-top: 1px solid var(--faint); margin-top: var(--sp-1); }
244320
245321 /* file and refs tables: bordered cards, rows inside */
246322 table.tree, table.refs {
323 background: var(--surface);
247324 border: 1px solid var(--line);
248325 border-radius: var(--r-lg);
249326 border-collapse: separate;
@@ -256,7 +333,7 @@ table.tree td, table.refs td {
256333 border-bottom: 1px solid var(--faint);
257334 }
258335 table.tree tr:last-child td, table.refs tr:last-child td { border-bottom: none; }
259table.tree tr:hover td, table.refs tr:hover td { background: var(--surface); }
336table.tree tr:hover td, table.refs tr:hover td { background: var(--hover); }
260337 table.tree tr.cols th {
261338 text-align: left;
262339 font-weight: 500;
@@ -264,9 +341,9 @@ table.tree tr.cols th {
264341 color: var(--muted);
265342 padding: var(--sp-1) var(--sp-4);
266343 border-bottom: 1px solid var(--line);
267 background: var(--surface);
344 background: var(--bg);
268345 }
269table.tree tr.cols:hover th { background: var(--surface); }
346table.tree tr.cols:hover th { background: var(--bg); }
270347 table.tree th.size { text-align: right; }
271348 table.tree td.name { width: 100%; }
272349 table.tree td.name a { color: var(--fg); }
@@ -286,7 +363,7 @@ table.refs td.sha { color: var(--muted); }
286363 border-bottom: 1px solid var(--faint);
287364 margin: calc(-1 * var(--sp-4)) calc(-1 * var(--sp-5)) var(--sp-4);
288365 padding: var(--sp-2) var(--sp-5);
289 background: var(--surface);
366 background: var(--bg);
290367 border-radius: var(--r-lg) var(--r-lg) 0 0;
291368 }
292369
@@ -295,6 +372,7 @@ ul.loglist {
295372 list-style: none;
296373 margin: var(--sp-3) 0;
297374 padding: 0;
375 background: var(--surface);
298376 border: 1px solid var(--line);
299377 border-radius: var(--r-lg);
300378 overflow: hidden;
@@ -307,7 +385,7 @@ ul.loglist li {
307385 border-bottom: 1px solid var(--faint);
308386 }
309387 ul.loglist li:last-child { border-bottom: none; }
310ul.loglist li:hover { background: var(--surface); }
388ul.loglist li:hover { background: var(--hover); }
311389 ul.loglist .commitmain { flex: 1; min-width: 0; }
312390 ul.loglist p { margin: 0; }
313391 ul.loglist .subject a { color: var(--fg); font-weight: 550; }
@@ -337,6 +415,7 @@ ul.repolist {
337415 list-style: none;
338416 margin: var(--sp-3) 0 var(--sp-5);
339417 padding: 0;
418 background: var(--surface);
340419 border: 1px solid var(--line);
341420 border-radius: var(--r-lg);
342421 overflow: hidden;
@@ -352,16 +431,17 @@ ul.pinlist {
352431 gap: var(--sp-2);
353432 }
354433 ul.pinlist li {
434 background: var(--surface);
355435 border: 1px solid var(--line);
356436 border-radius: var(--r-md);
357437 padding: var(--sp-1) var(--sp-3);
358438 }
359ul.pinlist li:hover { background: var(--surface); }
439ul.pinlist li:hover { background: var(--hover); }
360440 ul.pinlist li a { color: var(--fg); }
361441 ul.pinlist li a:hover { color: var(--accent); text-decoration: none; }
362442 ul.pinlist .sep { color: var(--muted); margin: 0 0.15em; }
363443 ul.repolist li:last-child { border-bottom: none; }
364ul.repolist li:hover { background: var(--surface); }
444ul.repolist li:hover { background: var(--hover); }
365445 ul.repolist li.empty { color: var(--muted); }
366446 ul.repolist p { margin: 0; }
367447 ul.repolist .reponame { font-size: var(--fs-3); }
@@ -451,7 +531,8 @@ button.linklike:hover { text-decoration: underline; filter: none; }
451531
452532 /* cards: README, comments */
453533 .readme, .code {
454 border: 1px solid var(--faint);
534 background: var(--surface);
535 border: 1px solid var(--line);
455536 border-radius: var(--r-lg);
456537 padding: var(--sp-4) var(--sp-5);
457538 margin-top: var(--sp-4);
@@ -605,13 +686,14 @@ nav.filters a {
605686 padding: 0.05rem var(--sp-2);
606687 border-radius: var(--r-pill);
607688 }
608nav.filters a:hover { color: var(--fg); background: var(--surface); text-decoration: none; }
689nav.filters a:hover { color: var(--fg); background: var(--hover); text-decoration: none; }
609690 nav.filters a.active { color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
610691
611692 ul.issuelist {
612693 list-style: none;
613694 margin: var(--sp-2) 0;
614695 padding: 0;
696 background: var(--surface);
615697 border: 1px solid var(--line);
616698 border-radius: var(--r-lg);
617699 overflow: hidden;
@@ -624,7 +706,7 @@ ul.issuelist li {
624706 border-bottom: 1px solid var(--faint);
625707 }
626708 ul.issuelist li:last-child { border-bottom: none; }
627ul.issuelist li:hover { background: var(--surface); }
709ul.issuelist li:hover { background: var(--hover); }
628710 ul.issuelist li.empty { display: block; color: var(--muted); }
629711 ul.issuelist .issuemain { flex: 1; min-width: 0; }
630712 ul.issuelist p { margin: 0; }
@@ -633,6 +715,7 @@ ul.issuelist .title a:hover { color: var(--accent); }
633715
634716 /* releases */
635717 article.release {
718 background: var(--surface);
636719 border: 1px solid var(--line);
637720 border-radius: var(--r-lg);
638721 padding: var(--sp-3) var(--sp-4);
internal/web/templates/layout.html +9 −5
@@ -34,19 +34,23 @@
3434 {{if .Repo.Settings.Website}}<p class="meta repowebsite"><a href="{{.Repo.Settings.Website}}" rel="nofollow">{{.Repo.Settings.Website}}</a></p>{{end}}
3535 {{if .Topics}}<p class="topics">{{range .Topics}}<span class="chip topic">{{.}}</span> {{end}}</p>{{end}}
3636 {{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>
37{{end}}<nav class="tabs">
37{{end}}</div>
38<nav class="tabs">
39 <span class="glabel">code</span>
3840 <a {{if eq .Tab "files"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">files</a>
3941 <a {{if eq .Tab "log"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log">log</a>
4042 <a {{if eq .Tab "refs"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/refs">refs</a>
41 <a {{if eq .Tab "releases"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/releases">releases</a>
42 <a {{if eq .Tab "builds"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/builds">builds</a>
43 <span class="glabel">work</span>
4344 <a {{if eq .Tab "issues"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues">issues</a>
4445 <a {{if eq .Tab "merge requests"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/mrs">merge requests</a>
46 <a {{if eq .Tab "builds"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/builds">builds</a>
47 <span class="glabel">publish</span>
48 <a {{if eq .Tab "releases"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/releases">releases</a>
4549 {{if .HasWiki}}<a {{if eq .Tab "wiki"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/wiki">wiki</a>
46 {{end}}<a {{if eq .Tab "search"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/search">search</a>
50 {{end}}<span class="glabel">tools</span>
51 <a {{if eq .Tab "search"}}class="active" {{end}}href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/search">search</a>
4752 <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/archive/{{.Ref}}.tar.gz">archive</a>
4853 </nav>
49</div>
5054 {{end}}
5155
5256 {{define "reporow"}}<li>
internal/web/web.go +3
@@ -21,6 +21,9 @@ var StyleCSS []byte
2121 //go:embed static/favicon.svg
2222 var FaviconSVG []byte
2323
24//go:embed static/fonts/*.woff2
25var FontFS embed.FS
26
2427 // version returns the short VCS revision baked into the binary, or "" when
2528 // built outside a checkout. Used by the layout footer.
2629 var version = sync.OnceValue(func() string {