feat: Swiss-style redesign, config overhaul, and project restructure - Complete Swiss-style CSS redesign: crimson red accent, heavy typography, pill-style nav, card-based sections, full dark mode - Templates restructured: repoheader moved inside <body>, nav close tags added, index grid with empty-description fallback - Replace yaml.v3 with viper for config loading (LEGIT_* env vars) - Replace stdlib flag with go-arg for argument parsing - Embed version and build date via ldflags (--version, --help) - Add GoReleaser config for cross-platform builds - Restructure to standard Go layout (cmd/legit/, internal/) - Migrate module path to codeberg.org/maxwelljensen/legit - Remove default config.yaml (replaced by viper defaults) - Fix log grid squashing, index date alignment, mode column width - Fix stray </a> in nav.html, text overlap with commit-info 💘 Generated with Crush
Maxwell Jensen 85795372+maxwelljens@users.noreply.github.com
Tue, 12 May 2026 17:40:39 +0200
12 files changed,
722 insertions(+),
179 deletions(-)
M
AGENTS.md
→
AGENTS.md
@@ -15,8 +15,9 @@ ## Essential Commands
| Action | Command | |--------|---------| -| Build | `go build -o legit` | -| Run | `go run . --config ./config.yaml` | +| Build | `go build -o legit ./cmd/legit` | +| Run | `go run ./cmd/legit --config ./config.yaml` | +| Release build | `goreleaser --snapshot --clean` | | Lint/Typecheck | `golangci-lint run` or `go vet ./...` | | Tests | `go test ./...` |@@ -25,41 +26,47 @@
## Project Structure ``` -├── main.go # Entrypoint: flags, config, unveil, serve -├── config/ -│ └── config.go # YAML config struct + reader -├── routes/ -│ ├── routes.go # Handlers: Index, RepoIndex, RepoTree, FileContent, Log, Diff, Refs, Archive, ServeStatic -│ ├── handler.go # Route registration via http.NewServeMux + git HTTP protocol multiplexing -│ ├── template.go # Template rendering helpers, chroma syntax highlighting, file display -│ ├── util.go # Helpers: getDisplayName, isGoModule, getDescription, isIgnored, isUnlisted, MIME helpers -│ └── git.go # Git HTTP smart protocol: InfoRefs(), UploadPack() via exec'd git-upload-pack -├── git/ -│ ├── git.go # GitRepo struct: Open, Commits, LastCommit, FileContent, Tags, Branches, FindMainBranch, WriteTar -│ ├── tree.go # FileTree listing, NiceTree struct -│ ├── diff.go # Diff/NiceDiff structs, go-gitdiff parsing -│ └── service/ -│ ├── service.go # git-upload-pack exec wrapper, pack-line/flush wire protocol -│ └── write_flusher.go # http.Flusher-aware writer for streaming smart protocol responses -├── templates/ # Go html/template files (10 templates) -├── static/ # style.css + legit.png favicon -├── config.yaml # Default config -├── contrib/ # Dockerfile, docker-compose.yml, systemd unit -├── flake.nix # Nix flake for build + Docker image -└── .github/workflows/ # Docker build + push to ghcr.io on master/tags +├── cmd/legit/ +│ ├── main.go # Entrypoint: args (go-arg), config (viper), unveil, serve +│ ├── unveil.go # OpenBSD unveil (build tag: openbsd) +│ └── unveil_stub.go # Unveil no-op (build tag: !openbsd) +├── internal/ +│ ├── config/ +│ │ └── config.go # YAML config struct + viper reader +│ ├── routes/ +│ │ ├── routes.go # Handlers: Index, RepoIndex, RepoTree, FileContent, Log, Diff, Refs, Archive, ServeStatic +│ │ ├── handler.go # Route registration via http.NewServeMux + git HTTP protocol multiplexing +│ │ ├── template.go # Template rendering helpers, chroma syntax highlighting, file display +│ │ ├── util.go # Helpers: getDisplayName, isGoModule, getDescription, isIgnored, isUnlisted, MIME helpers +│ │ └── git.go # Git HTTP smart protocol: InfoRefs(), UploadPack() via exec'd git-upload-pack +│ └── git/ +│ ├── git.go # GitRepo struct: Open, Commits, LastCommit, FileContent, Tags, Branches, FindMainBranch, WriteTar +│ ├── tree.go # FileTree listing, NiceTree struct +│ ├── diff.go # Diff/NiceDiff structs, go-gitdiff parsing +│ └── service/ +│ ├── service.go # git-upload-pack exec wrapper, pack-line/flush wire protocol +│ └── write_flusher.go # http.Flusher-aware writer for streaming smart protocol responses +├── templates/ # Go html/template files (10 templates) +├── static/ # style.css + legit.png favicon +├── assets/ # EUPL badge SVG +├── config.yaml # Default config +├── .goreleaser.yaml # GoReleaser release build config +├── contrib/ # Dockerfile, docker-compose.yml, systemd unit +├── flake.nix # Nix flake for build + Docker image +└── .github/workflows/ # Docker build + push to ghcr.io on master/tags ``` ## Application Architecture & Request Flow -1. **`main.go`**: reads YAML config, calls `UnveilPaths()` (no-op on non-OpenBSD via build tags), creates `http.ServeMux` from `routes.Handlers()`, starts HTTP server. +1. **`cmd/legit/main.go`**: parses args via `go-arg`, reads config via `viper` (with `LEGIT_` env var overrides), calls `UnveilPaths()` (no-op on non-OpenBSD via build tags), creates `http.ServeMux` from `routes.Handlers()`, starts HTTP server. Version info (`legit --version`) injected via goreleaser ldflags. -2. **Routing** (`routes/handler.go`): uses Go 1.22+ pattern-based `http.NewServeMux` with `{name}`, `{ref}`, `{rest...}` path values. A key pattern — the root `/{name}` and `/{name}/{rest...}` handlers go through **`Multiplex()`** which detects git HTTP smart protocol requests (`info/refs?service=git-upload-pack`, `git-upload-pack` POST) and routes them to git backend, else falls through to `RepoIndex` for web rendering. +2. **Routing** (`internal/routes/handler.go`): uses Go 1.22+ pattern-based `http.NewServeMux` with `{name}`, `{ref}`, `{rest...}` path values. A key pattern — the root `/{name}` and `/{name}/{rest...}` handlers go through **`Multiplex()`** which detects git HTTP smart protocol requests (`info/refs?service=git-upload-pack`, `git-upload-pack` POST) and routes them to git backend, else falls through to `RepoIndex` for web rendering. -3. **Git operations** (`git/` package): uses `go-git/v5` (pinned to v5.6.1 via replace directive in go.mod). `git.Open(path, ref)` resolves refs to hashes and returns a `GitRepo`. File browsing, commit log, tree listing all use the in-memory go-git API. +3. **Git operations** (`internal/git/` package): uses `go-git/v5` (pinned to v5.6.1 via replace directive in go.mod). `git.Open(path, ref)` resolves refs to hashes and returns a `GitRepo`. File browsing, commit log, tree listing all use the in-memory go-git API. -4. **Git HTTP smart protocol** (`routes/git.go` + `git/service/`): shells out to `git-upload-pack --stateless-rpc` for clone/fetch over HTTPS. **Push is explicitly blocked** at the handler level with `"no pushing allowed!"`. +4. **Git HTTP smart protocol** (`internal/routes/git.go` + `internal/git/service/`): shells out to `git-upload-pack --stateless-rpc` for clone/fetch over HTTPS. **Push is explicitly blocked** at the handler level with `"no pushing allowed!"`. -5. **Archive downloads** (`routes.go:Archive`): streams tar.gz on-the-fly via `GitRepo.WriteTar()` + `compress/gzip`. +5. **Archive downloads** (`internal/routes/routes.go:Archive`): streams tar.gz on-the-fly via `GitRepo.WriteTar()` + `compress/gzip`. 6. **Templates**: all rendered via `template.ParseGlob(tpath)` per request (not cached — re-parsed every time). Templates are split into `define` blocks: `head`, `nav`, `repoheader`, `index`, `repo`, `tree`, `file`, `log`, `commit`, `refs`, `404`, `500`.@@ -67,7 +74,6 @@ ## Key Gotchas
- **`go.mod replace directives`**: `go-git/v5` is pinned to v5.6.1 (not v5.13.x as in go.sum) via a replace directive. `go-gitdiff` has `sergi/go-diff` replaced to v1.1.0. These overrides are intentional — don't remove them. - **No template caching**: `template.ParseGlob` is called per-request in every handler. This is by design (hot-reload friendly) but a perf footgun. -- **`getAllRepos()` is unused** — leftover code that does recursive repo scanning (bare repo detection via HEAD file). The active Index handler reads repos at the top level of `scanPath` only. - **`description` file**: per-repo descriptions are read from a plain `description` file at the repo root, not from git config. - **`securejoin` everywhere**: all path construction from user input (repo name, file paths) uses `github.com/cyphar/filepath-securejoin` — a security-critical pattern. Never use raw `filepath.Join` with user input. - **Static path traversal**: `ServeStatic` goes through `securejoin` too — `{file}` pattern is safe.@@ -123,5 +129,5 @@ ## Deploy
- Run behind TLS-terminating proxy (nginx, relayd). - Systemd unit at `contrib/legit.service` with `ProtectSystem=strict`. -- Docker images: `ghcr.io/icyphox/legit:{master,latest,vX.Y.Z}`. +- Docker images: `ghcr.io/<owner>/legit:{master,latest,vX.Y.Z}` (uses `${{ github.repository }}` from the workflow). - Docker compose at `contrib/docker-compose.yml` mounts repos, config, templates, and static from host.
M
CHANGELOG.md
→
CHANGELOG.md
@@ -6,6 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Viper config loading with environment variable overrides (`LEGIT_*` prefix) +- Build-time version and date embedding via ldflags, shown in `--help` and + `--version` +- GoReleaser configuration for cross-platform builds + +### Changed + +- Complete Swiss-style CSS redesign: crimson red accent, heavy typography, + pill-style nav buttons, card-based sections with left red borders, full dark + mode, responsive mobile layout +- Project restructured to standard Go layout (`cmd/legit/`, `internal/`) +- Module path migrated to `codeberg.org/maxwelljensen/legit` +- Config library switched from `gopkg.in/yaml.v3` to viper +- Flag parsing switched from stdlib `flag` to `go-arg` +- Templates restructured: `repoheader` moved inside `<body>` in all 6 + templates, missing `</li>` close tags added to nav elements + +### Fixed + +- Log page: commit message column no longer squashed by `commit-info` + (widened message column to 1.3fr, reduced padding, tightened + commit-info line-height) +- Index page: empty descriptions no longer break date alignment (` ` + fallback, restructured grid with `.index-idle` class) +- Tree page: mode column now handles long mode strings without wrapping + (`minmax(10ch, auto)`) +- Text overlapping with `commit-info` on long commit messages resolved +- nav.html: stray `</a>` tag after `</li>` on refs link removed +- Minor README formatting errors corrected + ## [0.2.6] - 2026-05-12 ### Added
D
config.yaml
@@ -1,20 +0,0 @@
-repo: - scanPath: /var/git - readme: - - readme - - README - - readme.md - - README.md - mainBranch: - - master - - main -dirs: - templates: ./templates - static: ./static -meta: - title: icy does git - description: come get your free software -server: - name: git.icyphox.sh - host: 0.0.0.0 - port: 5555
M
static/style.css
→
static/style.css
@@ -1,29 +1,34 @@
:root { + --red: #c1272d; + --red-dark: #a02026; + --red-light: #e8494f; --white: #fff; - --light: #f4f4f4; - --cyan: #509c93; - --light-gray: #eee; - --medium-gray: #ddd; - --gray: #6a6a6a; + --light: #f4f4f2; + --light-gray: #e8e8e4; + --medium-gray: #c8c8c4; + --gray: #888; --dark: #444; - --darker: #222; + --darker: #1a1a1a; - --sans-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Segoe UI", sans-serif; - --display-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Segoe UI", sans-serif; - --mono-font: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', 'Roboto Mono', Menlo, Consolas, monospace; + --sans-font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --mono-font: "JetBrains Mono", "SF Mono", SFMono-Regular, ui-monospace, "DejaVu Sans Mono", "Roboto Mono", Menlo, Consolas, monospace; + + --border-radius: 4px; } @media (prefers-color-scheme: dark) { :root { color-scheme: dark light; - --light: #181818; - --cyan: #76c7c0; - --light-gray: #333; - --medium-gray: #444; - --gray: #aaa; - --dark: #ddd; - --darker: #f4f4f4; - --white: #000; + --red: #e8494f; + --red-dark: #c1272d; + --red-light: #f06a6f; + --white: #0d0d0d; + --light: #1a1a1a; + --light-gray: #2a2a2a; + --medium-gray: #3a3a3a; + --gray: #888; + --dark: #ccc; + --darker: #eee; } }@@ -31,16 +36,19 @@ html {
background: var(--white); -webkit-text-size-adjust: none; font-family: var(--sans-font); - font-weight: 380; + font-weight: 400; + font-size: 15px; + line-height: 1.6; } -pre { +pre, code, kbd, samp { font-family: var(--mono-font); } ::selection { - background: var(--medium-gray); - opacity: 0.3; + background: var(--red); + color: #fff; + opacity: 0.85; } * {@@ -51,133 +59,402 @@ }
body { max-width: 1000px; - padding: 0 13px; - margin: 40px auto; + padding: 0 20px; + margin: 0 auto 60px; + border-top: 6px solid var(--red); } -main, footer { - font-size: 1rem; - padding: 0; - line-height: 160%; +/* ── Typography ── */ + +h1, h2, h3, h4 { + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.2; } -header h1, h2, h3 { - font-family: var(--display-font); +h1 { + font-size: 2rem; + padding: 1.5rem 0 0.25rem; } h2 { - font-weight: 400; + font-size: 1.5rem; + padding: 1.25rem 0 0.25rem; +} + +h3 { + font-size: 1.1rem; + padding: 1.5rem 0 0.5rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--gray); + font-weight: 600; } strong { - font-weight: 500; + font-weight: 600; +} + +/* ── Header ── */ + +header { + padding: 1rem 0 0.5rem; +} + +header h1 { + font-size: 2.2rem; + font-weight: 800; + letter-spacing: -0.03em; + padding: 0; } -main h1 { - padding: 10px 0 10px 0; +header h1 a { + color: var(--darker); + border: none; +} + +header h1 a:hover { + border: none; + color: var(--red); } -main h2 { - font-size: 18px; +header h2 { + font-size: 1.3rem; + font-weight: 700; + padding: 0.5rem 0 0; +} + +header .ref { + font-family: var(--mono-font); + font-size: 0.85rem; + font-weight: 400; + color: var(--gray); + display: inline-block; + padding-top: 0.2em; + letter-spacing: 0; } -main h2, h3 { - padding: 20px 0 15px 0; +header .desc { + color: var(--gray); + font-weight: 400; + font-style: italic; + font-size: 1rem; + padding: 0.25rem 0 0.5rem; } +/* ── Navigation ── */ + nav { - padding: 0.4rem 0 1.5rem 0; + padding: 0.5rem 0 1.5rem; + border-bottom: 2px solid var(--light-gray); + margin-bottom: 0.5rem; } nav ul { - padding: 0; - margin: 0; list-style: none; - padding-bottom: 20px; + display: flex; + gap: 0.25rem; + flex-wrap: wrap; } nav ul li { - padding-right: 10px; + display: inline; +} + +nav ul li a { display: inline-block; + padding: 0.3rem 0.8rem; + font-size: 0.85rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--darker); + border: 2px solid var(--medium-gray); + border-radius: var(--border-radius); + transition: all 0.15s ease; } +nav ul li a:hover { + color: var(--red); + border-color: var(--red); +} + +nav ul li:first-child a { + border-color: var(--red); + color: var(--red); +} + +nav ul li:first-child a:hover { + background: var(--red); + color: #fff; +} + +/* ── Links ── */ + a { - margin: 0; - padding: 0; - box-sizing: border-box; text-decoration: none; + color: var(--darker); word-wrap: break-word; + transition: color 0.15s ease; } -a { - color: var(--darker); - border-bottom: 1.5px solid var(--medium-gray); +a:hover { + color: var(--red); +} + +main a { + font-weight: 500; } -a:hover { - border-bottom: 1.5px solid var(--gray); +main a:hover { + border-bottom: 2px solid var(--red); } +/* ── Index Page ── */ + .index { - padding-top: 2em; + padding-top: 1.5rem; display: grid; - grid-template-columns: 6em 1fr minmax(0, 7em); - grid-row-gap: 0.5em; + grid-template-columns: 1fr auto; + grid-row-gap: 0.15em; min-width: 0; } +.index-name { + font-weight: 600; + font-size: 1.05rem; + padding: 0.4rem 0; + grid-column: 1; +} + +.index-name a { + border-left: 3px solid var(--red); + padding-left: 0.75rem; + transition: border-width 0.1s ease; +} + +.index-name a:hover { + border-left-width: 6px; + color: var(--red); + border-bottom: none; +} + +.index-idle { + text-align: right; + color: var(--gray); + font-size: 0.85rem; + font-family: var(--mono-font); + padding: 0.5rem 0; + grid-column: 2; +} + +.index .desc { + grid-column: 1 / -1; + font-weight: 400; + color: var(--gray); + font-style: italic; + font-size: 0.9rem; + padding: 0 0 0.6rem 1rem; + border-bottom: 1px solid var(--light-gray); + margin-bottom: 0.2rem; + min-height: 1.2em; +} + +.index > div:nth-child(3n) { + grid-column: 2; +} + +.index > div:nth-child(3n-1) { + grid-column: 1 / -1; +} + +.index > div:nth-child(1) { + grid-column: 1; +} + +.index > div:nth-child(2) { + grid-column: 1 / -1; +} + +/* clone url */ + .clone-url { - padding-top: 2rem; + padding: 2rem 0 0.5rem; +} + +.clone-url strong { + display: block; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--gray); + padding-bottom: 0.4rem; } .clone-url pre { + background: var(--light); + padding: 0.75rem 1rem; + border-radius: var(--border-radius); + border-left: 3px solid var(--red); + font-size: 0.85rem; color: var(--dark); white-space: pre-wrap; + overflow-x: auto; } -.desc { - font-weight: normal; - color: var(--gray); - font-style: italic; -} +/* ── File Tree ── */ .tree { display: grid; - grid-template-columns: 10ch auto 1fr; - grid-row-gap: 0.5em; + grid-template-columns: minmax(10ch, auto) 8ch 1fr; + grid-row-gap: 0.15em; grid-column-gap: 1em; min-width: 0; } -.log { - display: grid; - grid-template-columns: 20rem minmax(0, 1fr); - grid-row-gap: 0.8em; - grid-column-gap: 8rem; - margin-bottom: 2em; - padding-bottom: 1em; - border-bottom: 1.5px solid var(--medium-gray); +.tree > div { + padding: 0.25rem 0; + border-bottom: 1px solid var(--light-gray); +} + +.tree > div:nth-child(3n+1) { + font-family: var(--mono-font); + font-size: 0.8rem; + color: var(--gray); +} + +.tree > div:nth-child(3n+2) { + font-family: var(--mono-font); + font-size: 0.8rem; + color: var(--gray); + text-align: right; +} + +.tree > div:nth-child(3n) { + font-weight: 500; +} + +.tree > div:nth-child(3n) a { + border-bottom: 1px solid transparent; } -.log pre { - white-space: pre-wrap; +.tree > div:nth-child(3n) a:hover { + border-bottom: 1px solid var(--red); } .mode, .size { font-family: var(--mono-font); } + .size { text-align: right; } -.readme pre { +/* ── Commit Log ── */ + +.log { + display: grid; + grid-template-columns: minmax(0, 1.3fr) minmax(14rem, 1fr); + grid-row-gap: 0; +} + +.log > div { + padding: 0.6rem 0; +} + +.log > div:nth-child(4n+1), +.log > div:nth-child(4n+2) { + border-bottom: 1px solid var(--light-gray); + padding-bottom: 0.8rem; +} + +.log > div:nth-child(4n+3) { + grid-column: 1; +} + +.log > div:nth-child(4n+4) { + grid-column: 2; +} + +.log > div:nth-child(4n+3), +.log > div:nth-child(4n+4) { + border-bottom: 1px solid var(--light-gray); + padding-top: 0; +} + +.log > div:nth-child(1), +.log > div:nth-child(2) { + padding-top: 0; +} + +.log > div:nth-child(4n+1) .commit-hash { + font-family: var(--mono-font); + font-size: 0.85rem; + font-weight: 600; + color: var(--red); +} + +.log > div:nth-child(4n+1) .commit-hash:hover { + text-decoration: underline; +} + +.log > div:nth-child(4n+1) pre { + font-family: var(--sans-font); + font-weight: 600; + font-size: 1rem; + padding-top: 0.2rem; white-space: pre-wrap; - overflow-x: auto; + overflow: hidden; } +.log > div:nth-child(4n+2) { + text-align: right; + font-size: 0.8rem; + color: var(--gray); + padding-top: 0.1rem; + line-height: 1.4; +} + +.log > div:nth-child(4n+2) .commit-email { + display: none; +} + +.log > div:nth-child(4n+4) { + font-size: 0.8rem; + color: var(--gray); + padding-top: 0.2rem; + line-height: 1.4; +} + +/* ── Readme ── */ + .readme { - background: var(--light-gray); - padding: 0.5rem; + margin: 2rem 0 1rem; + padding: 1.5rem; + background: var(--light); + border-radius: var(--border-radius); + border-left: 3px solid var(--red); +} + +.readme h1, +.readme h2, +.readme h3 { + text-transform: none; + letter-spacing: 0; + color: var(--darker); +} + +.readme h1 { font-size: 1.5rem; } +.readme h2 { font-size: 1.25rem; } +.readme h3 { font-size: 1.1rem; } + +.readme pre { + white-space: pre-wrap; + overflow-x: auto; + background: var(--white); + padding: 0.75rem; + border-radius: 3px; + font-size: 0.85rem; } .readme ul {@@ -188,75 +465,220 @@ .readme img {
max-width: 100%; } +.readme a { + color: var(--red); + font-weight: 500; +} + +.readme a:hover { + border-bottom: 2px solid var(--red); +} + +.readme code { + font-size: 0.85em; + background: var(--white); + padding: 0.15em 0.3em; + border-radius: 3px; +} + +/* ── Diff / Commit ── */ + .diff { - margin: 1rem 0 1rem 0; - padding: 1rem 0 1rem 0; - border-bottom: 1.5px solid var(--medium-gray); + margin: 1.5rem 0; + padding: 1rem 0 1rem; + border-top: 2px solid var(--light-gray); +} + +.diff:first-child { + border-top: none; } .diff pre { - overflow: scroll; + overflow-x: auto; + font-size: 0.85rem; + line-height: 1.5; + background: var(--light); + padding: 1rem; + border-radius: var(--border-radius); } .diff-stat { - padding: 1rem 0 1rem 0; + padding: 1rem 0; + font-size: 0.9rem; + color: var(--dark); } -.commit-hash, .commit-email { +.diff-stat div:first-child { + font-weight: 600; + padding-bottom: 0.5rem; +} + +.diff-stat strong { + display: block; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--gray); + padding: 0.5rem 0 0.25rem; +} + +.diff-stat ul { + list-style: none; +} + +.diff-stat ul li { + padding-left: 0.5em; + font-size: 0.85rem; + line-height: 1.6; +} + +.diff-stat ul li a { + font-weight: 400; +} + +.commit-hash { font-family: var(--mono-font); } +.commit-email { + font-family: var(--mono-font); + font-size: 0.85em; + color: var(--gray); +} + .commit-email:before { - content: '<'; + content: "<"; } .commit-email:after { - content: '>'; + content: ">"; } .commit { - margin-bottom: 1rem; + margin-bottom: 1.5rem; } .commit pre { - padding-bottom: 1rem; + font-family: var(--sans-font); + font-weight: 600; + font-size: 1.1rem; + padding-bottom: 0.75rem; white-space: pre-wrap; } -.diff-stat ul li { - list-style: none; - padding-left: 0.5em; +.commit-info { + color: var(--gray); + padding-bottom: 1.5rem; + font-size: 0.85rem; + line-height: 1.6; +} + +.commit .commit-info { + padding-bottom: 0; +} + +.commit strong { + display: inline-block; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--gray); + min-width: 5rem; +} + +.commit p { + display: inline; + font-family: var(--mono-font); + font-size: 0.85rem; + word-break: break-all; +} + +.commit p a { + font-weight: 400; } .diff-add { - color: green; + color: #2e7d32; +} + +@media (prefers-color-scheme: dark) { + .diff-add { + color: #66bb6a; + } } .diff-del { - color: red; + color: #c62828; +} + +@media (prefers-color-scheme: dark) { + .diff-del { + color: #ef5350; + } } .diff-noop { color: var(--gray); } -.ref { - font-family: var(--sans-font); - font-size: 14px; - color: var(--gray); +.diff-type { display: inline-block; - padding-top: 0.7em; + font-family: var(--mono-font); + font-weight: 700; + font-size: 0.8rem; + padding: 0.1rem 0.4rem; + border-radius: 3px; + background: var(--light-gray); + color: var(--dark); + margin-right: 0.5rem; +} + +/* ── Refs (branches & tags) ── */ + +.refs { + margin-bottom: 1rem; +} + +.refs > div { + padding: 0.6rem 0.75rem; + border-bottom: 1px solid var(--light-gray); + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; } -.refs pre { - white-space: pre-wrap; - padding-bottom: 0.5rem; +.refs > div:hover { + background: var(--light); } .refs strong { - padding-right: 1em; + font-family: var(--mono-font); + font-size: 0.9rem; + font-weight: 600; + min-width: 12rem; +} + +.refs a { + font-size: 0.85rem; + font-weight: 500; + color: var(--gray); +} + +.refs a:hover { + color: var(--red); } +.refs pre { + width: 100%; + font-size: 0.85rem; + white-space: pre-wrap; + color: var(--gray); + padding: 0.5rem 0 0; +} + +/* ── File View ── */ + .line-numbers { white-space: pre-line; -moz-user-select: -moz-none;@@ -268,6 +690,22 @@ display: flex;
float: left; flex-direction: column; margin-right: 1ch; + font-size: 0.8rem; + color: var(--gray); + text-align: right; + padding-right: 0.75rem; + border-right: 2px solid var(--light-gray); + min-width: 3.5rem; +} + +.line-numbers a { + color: var(--gray); + font-weight: 400; +} + +.line-numbers a:hover { + color: var(--red); + border: none; } .file-wrapper {@@ -276,7 +714,9 @@ flex-direction: row;
grid-template-columns: 1rem minmax(0, 1fr); gap: 1rem; padding: 0.5rem; - background: var(--light-gray); + background: var(--light); + border-radius: var(--border-radius); + border: 1px solid var(--light-gray); overflow-x: auto; }@@ -285,48 +725,133 @@ display: flex;
flex-direction: row; grid-template-columns: 1rem minmax(0, 1fr); overflow-x: auto; + padding: 0.5rem; + background: var(--light); + border-radius: var(--border-radius); + border: 1px solid var(--light-gray); } .file-content { - background: var(--light-gray); + background: var(--light); overflow-y: hidden; overflow-x: auto; + font-size: 0.85rem; } -.diff-type { +.file-content pre { + white-space: pre; +} + +main > p:first-child { + font-size: 0.9rem; color: var(--gray); + padding-bottom: 1rem; + border-bottom: 1px solid var(--light-gray); + margin-bottom: 1rem; } -.commit-info { +main > p:first-child a { + color: var(--red); + font-weight: 500; +} + +/* ── Error Pages ── */ + +main h3:only-child { + text-align: center; + padding: 3rem 0; + font-size: 1.3rem; color: var(--gray); - padding-bottom: 1.5rem; - font-size: 0.85rem; + text-transform: none; + letter-spacing: 0; } +/* ── Responsive ── */ + @media (max-width: 600px) { + body { + padding: 0 12px; + } + + h1 { + font-size: 1.5rem; + } + .index { - grid-row-gap: 0.8em; + grid-template-columns: 1fr; + } + + .index-name { + border-bottom: 1px solid var(--light-gray); + } + + .index .desc { + padding-bottom: 0.6rem; + margin-bottom: 0; + } + + .index-idle { + grid-column: 1; + text-align: left; + padding: 0 0 0.5rem 1rem; + border-bottom: 1px solid var(--light-gray); + } + + .index > div:nth-child(3n), + .index > div:nth-child(3n-1), + .index > div:nth-child(1), + .index > div:nth-child(2) { + grid-column: 1; } .log { grid-template-columns: 1fr; - grid-row-gap: 0em; } - .index { - grid-template-columns: 1fr; - grid-row-gap: 0em; + .log > div:nth-child(4n+3) { + grid-column: 1; } - .index-name:not(:first-child) { - padding-top: 1.5rem; + .log > div:nth-child(4n+4) { + grid-column: 1; } - .commit-info:not(:last-child) { - padding-bottom: 1.5rem; + .tree { + grid-template-columns: 1fr; + } + + .tree > div:nth-child(3n+1), + .tree > div:nth-child(3n+2) { + display: none; } pre { font-size: 0.8rem; + } + + nav ul { + gap: 0.2rem; + } + + nav ul li a { + font-size: 0.78rem; + padding: 0.2rem 0.6rem; + } + + .refs strong { + min-width: 8rem; + font-size: 0.85rem; + } + + .commit { + font-size: 0.9rem; + } + + header h1 { + font-size: 1.6rem; + } + + header h2 { + font-size: 1.1rem; } }
M
templates/commit.html
→
templates/commit.html
@@ -3,8 +3,8 @@ <!doctype html>
<html> {{ template "head" . }} - {{ template "repoheader" . }} <body> + {{ template "repoheader" . }} {{ template "nav" . }} <main> <section class="commit">
M
templates/file.html
→
templates/file.html
@@ -2,8 +2,8 @@ {{ define "file" }}
<!doctype html> <html> {{ template "head" . }} - {{ template "repoheader" . }} <body> + {{ template "repoheader" . }} {{ template "nav" . }} <main> <p>{{ .path }} (<a style="color: gray" href="?raw=true">view raw</a>)</p>
M
templates/index.html
→
templates/index.html
@@ -12,8 +12,8 @@ <main>
<div class="index"> {{ range .info }} <div class="index-name"><a href="/{{ .Name }}">{{ .DisplayName }}</a></div> - <div class="desc">{{ .Desc }}</div> - <div>{{ .Idle }}</div> + <div class="desc">{{ if .Desc }}{{ .Desc }}{{ else }} {{ end }}</div> + <div class="index-idle">{{ .Idle }}</div> {{ end }} </div> </main>
M
templates/log.html
→
templates/log.html
@@ -3,8 +3,8 @@ <!doctype html>
<html> {{ template "head" . }} - {{ template "repoheader" . }} <body> + {{ template "repoheader" . }} {{ template "nav" . }} <main> {{ $repo := .name }}
M
templates/refs.html
→
templates/refs.html
@@ -3,8 +3,8 @@ <!doctype html>
<html> {{ template "head" . }} - {{ template "repoheader" . }} <body> + {{ template "repoheader" . }} {{ template "nav" . }} <main> {{ $name := .name }}
M
templates/repo.html
→
templates/repo.html
@@ -3,9 +3,8 @@ <!doctype html>
<html> {{ template "head" . }} -{{ template "repoheader" . }} - <body> + {{ template "repoheader" . }} {{ template "nav" . }} <main> {{ $repo := .name }}
M
templates/tree.html
→
templates/tree.html
@@ -1,11 +1,10 @@
{{ define "tree" }} <!doctype html> <html> - {{ template "head" . }} - {{ template "repoheader" . }} <body> + {{ template "repoheader" . }} {{ template "nav" . }} <main> {{ $repo := .name }}