Clean up the design and make the layout fluid

Layout
- Viewport is width=device-width. The fixed 500px minimum is gone.
  Every view works down to 320px without horizontal scrolling.
- Body keeps a 320px floor. Narrower windows scroll instead of
  squeezing the layout.
- No media queries. Wrapping uses flex-wrap, clamp(), auto-fit grids
  and overflow-wrap. The one container query keeps the diff file nav
  sticky only when it sits beside the diffs.
- Body grid column is minmax(0, 1fr), so wide content can no longer
  stretch the page. Header shrinks the user name first, then the logo.
- Popups anchor safely on narrow screens. The file delete popup is
  anchored to the blob header. The reaction picker opens downward and
  the comment card no longer clips it.

Style
- Tokens for a fluid gutter, info colour and popup shadow. Shared
  :focus-visible outline. Body line-height 1.5.
- Clone is a secondary button. Small buttons use a sans font.
- Issue list shows the number small and muted. Labels and meta align
  with the title.
- Release rows show the tag badge in the meta line. Notes under five
  lines render in full without a "Show more" toggle.
- Branch and tag rows keep their actions in the name row.
- Edited comments show "· edited" after the date instead of a leading
  star.
- Markdown tables scroll instead of overflowing. Long tokens wrap.
- CI pills use theme colours. Undefined CSS variables in the CI views
  are fixed. The blob :target highlight follows data-theme.
- Dead utility classes and unused rules are removed.

Raw files
- HTML, SVG and XML raw files keep their real Content-Type and get a
  sandbox CSP with no scripts and an opaque origin, instead of being
  served as text/plain. SVGs now render in READMEs and the blob view.

Tests
- Seed repo has a logo.svg. New unit and E2E tests cover the raw
  sandbox headers. The edited-marker test uses the new selector.
- scripts/seed-design.sh seeds a data dir with content for every view.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AuthorKonata <konata@posteo.jp>
Date
Commit13eaff244b85b099ecebf2d43afa3a11939d2521
Parent6a65f41
16 files changed, 1599 insertions(+), 1048 deletions(-)
▾MREADME.md
@@ -1,4 +1,4 @@
# Hearthforge ![](public/assets/favicon.svg)
# Hearthforge ![](web/static/assets/favicon.svg)
![](preview.png)
▾Minternal/web/repos_files.go
@@ -20,21 +20,25 @@ import (
"hearthforge/internal/web/views"
)
// rawInertTypes are served as plain text from /raw. An HTML or SVG document
// on our own origin could load another raw file as a same-origin script, which
// is a stored-XSS path. Every other type keeps its real MIME so previews work.
var rawInertTypes = map[string]bool{
"text/html": true,
"application/xhtml+xml": true,
"image/svg+xml": true,
}
func rawServeContentType(contentType string) string {
// rawSandboxCSP is sent with every /raw response the browser would parse as a
// document (HTML, SVG, XML). `sandbox` without allow-same-origin gives the
// document an opaque origin: no cookies, no storage, and no readable fetch
// of anything on this server. No allow-scripts, so nothing runs at all.
// A raw file on our origin could otherwise act as the viewer, which is a
// stored-XSS path. Every other type gets no policy, so previews work.
const rawSandboxCSP = "sandbox; default-src 'none'; img-src 'self' data:; " +
"style-src 'unsafe-inline'; font-src 'self' data:; frame-ancestors 'none'"
// isDocumentType reports whether a browser parses this MIME type as a
// scripting document. It reads the declared type, the same value that goes
// into Content-Type, so the render and sandbox decisions cannot drift apart.
func isDocumentType(contentType string) bool {
base := strings.ToLower(strings.TrimSpace(strings.SplitN(contentType, ";", 2)[0]))
if rawInertTypes[base] {
return "text/plain; charset=utf-8"
switch base {
case "text/html", "application/xhtml+xml", "image/svg+xml", "text/xml", "application/xml":
return true
}
return contentType
return strings.HasSuffix(base, "+xml")
}
// treeRoot lists the repository root at a ref.
@@ -154,7 +158,7 @@ func (s *Server) rawFile(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not found", http.StatusNotFound)
return
}
contentType := rawServeContentType(sniffContentType(filename, head))
contentType := sniffContentType(filename, head)
body, stop, err := s.blobStream(r, repo.Name, ref, filePath)
if err != nil {
@@ -167,6 +171,9 @@ func (s *Server) rawFile(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("Content-Type", contentType)
h.Set("Accept-Ranges", "bytes")
if isDocumentType(contentType) {
h.Set("Content-Security-Policy", rawSandboxCSP)
}
if partial {
h.Set("Content-Range", "bytes "+strconv.FormatInt(start, 10)+"-"+
strconv.FormatInt(start+length-1, 10)+"/"+strconv.FormatInt(total, 10))
▾Minternal/web/repos_test.go
@@ -267,3 +267,57 @@ func TestRefParamDecodesOnce(t *testing.T) {
}
}
}
func TestRawDocumentTypesAreSandboxed(t *testing.T) {
h, _, admin := repoTestServer(t)
do(t, h, admin, "POST", "/new", url.Values{"name": {"docs"}, "default_branch": {"main"}})
do(t, h, admin, "POST", "/docs/new-file/main", url.Values{
"path": {"logo.svg"},
"content": {`<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>`},
})
do(t, h, admin, "POST", "/docs/new-file/main", url.Values{
"path": {"page.html"}, "content": {"<!doctype html><script>alert(1)</script>"},
})
do(t, h, admin, "POST", "/docs/new-file/main", url.Values{
"path": {"notes.txt"}, "content": {"plain\n"},
})
for _, tc := range []struct {
path, wantType string
sandboxed bool
}{
{"/docs/raw/main/logo.svg", "image/svg+xml", true},
{"/docs/raw/main/page.html", "text/html", true},
{"/docs/raw/main/notes.txt", "text/plain", false},
} {
res := do(t, h, admin, "GET", tc.path, nil)
if res.Code != http.StatusOK {
t.Fatalf("%s: status = %d", tc.path, res.Code)
}
if ct := res.Header().Get("Content-Type"); !strings.HasPrefix(ct, tc.wantType) {
t.Errorf("%s: Content-Type = %q, want prefix %q", tc.path, ct, tc.wantType)
}
csp := res.Header().Get("Content-Security-Policy")
if tc.sandboxed && !strings.Contains(csp, "sandbox;") {
t.Errorf("%s: missing sandbox CSP, got %q", tc.path, csp)
}
if !tc.sandboxed && csp != "" {
t.Errorf("%s: unexpected CSP %q", tc.path, csp)
}
}
}
func TestIsDocumentType(t *testing.T) {
for in, want := range map[string]bool{
"text/html; charset=utf-8": true,
"IMAGE/SVG+XML": true,
"application/rss+xml": true,
"text/plain": false,
"image/png": false,
"application/json": false,
} {
if got := isDocumentType(in); got != want {
t.Errorf("isDocumentType(%q) = %v, want %v", in, got, want)
}
}
}
▾Minternal/web/views/datewithedited.go
@@ -7,14 +7,16 @@ import (
. "maragu.dev/gomponents/html"
)
// DateWithEdited renders a date. It marks edited items with a leading star
// and an "Edited ..." tooltip.
// DateWithEdited renders a date. Edited items get an "edited" mark with the
// edit time in its tooltip.
func DateWithEdited(date string, editedAt *string) g.Node {
label := util.FormatDate(date)
if editedAt == nil {
return g.El("time", DateTime(date), g.Text(label))
}
return g.El("time", DateTime(date), Class("edited-indicator"),
Title("Edited "+util.FormatDateTime(*editedAt)),
g.Text("*"+label))
return g.Group{
g.El("time", DateTime(date), g.Text(label)),
Span(Class("edited-indicator"), Title("Edited "+util.FormatDateTime(*editedAt)),
g.Text("· edited")),
}
}
▾Minternal/web/views/layout.go
@@ -35,7 +35,7 @@ func Layout(p Page, body ...g.Node) g.Node {
return HTML(Lang("en"),
Head(
Meta(Charset("UTF-8")),
Meta(Name("viewport"), Content("width=500")),
Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
TitleEl(g.Text(pageTitle)),
Script(Src("/assets/theme.js")),
Link(Rel("icon"), Type("image/svg+xml"), Href("/assets/favicon.svg")),
▾Minternal/web/views/releases.go
@@ -2,6 +2,7 @@ package views
import (
"strconv"
"strings"
"hearthforge/internal/config"
"hearthforge/internal/db"
@@ -63,28 +64,19 @@ func ReleaseList(cfg *config.Config, user *db.SessionUser, repo *db.Repo,
Style("view-transition-name: release-title-"+id),
g.Text(rel.Name)),
Div(Class("release-item-meta"),
g.If(deref(rel.TagName) != "",
A(Href("/"+repo.Name+"/tree/"+EscapePath(deref(rel.TagName))), Class("badge"),
g.Text(deref(rel.TagName)))),
g.If(item.AssetCount > 0,
Span(g.Text(strconv.Itoa(item.AssetCount)+" "+assetWord(item.AssetCount)))),
g.If(rel.IncludeSourceCode, Span(g.Text("source archives"))),
),
),
Div(Class("release-item-date"),
g.If(deref(rel.TagName) != "",
A(Href("/"+repo.Name+"/tree/"+EscapePath(deref(rel.TagName))), Class("badge"),
g.Text(deref(rel.TagName)))),
g.El("time", DateTime(rel.CreatedAt), g.Text(util.FormatDate(rel.CreatedAt))),
),
),
g.If(deref(rel.Notes) != "", Div(Class("release-notes-section"),
P(Class("release-notes-label"), g.Text("Release Notes")),
Details(Class("notes-expand"),
Summary(Class("notes-toggle"),
Div(Class("notes-preview"), g.Text(item.NotesPlain)),
Span(Class("notes-toggle-label")),
),
Div(Class("notes-full markdown-body"), g.Raw(item.NotesHTML)),
),
)),
g.If(deref(rel.Notes) != "", releaseNotes(item)),
)
}),
)),
@@ -93,6 +85,26 @@ func ReleaseList(cfg *config.Config, user *db.SessionUser, repo *db.Repo,
)
}
// releaseNotes renders the notes of a list row. Short notes show in full.
// Longer notes show a five-line preview behind a "Show more" toggle.
func releaseNotes(item ReleaseListItem) g.Node {
long := strings.Count(item.NotesPlain, "\n") >= 5 || len(item.NotesPlain) > 320
body := Div(Class("markdown-body"), g.Raw(item.NotesHTML))
if long {
body = Details(Class("notes-expand"),
Summary(Class("notes-toggle"),
Div(Class("notes-preview"), g.Text(item.NotesPlain)),
Span(Class("notes-toggle-label")),
),
Div(Class("notes-full markdown-body"), g.Raw(item.NotesHTML)),
)
}
return Div(Class("release-notes-section"),
P(Class("release-notes-label"), g.Text("Release Notes")),
body,
)
}
// assetWord pluralizes the asset count label.
func assetWord(n int) string {
if n == 1 {
▾Minternal/web/views/repos.go
@@ -217,6 +217,10 @@ func BranchList(cfg *config.Config, user *db.SessionUser, repo *db.Repo, branche
Div(Class("ref-name-row"),
A(Href("/"+repo.Name+"/tree/"+EscapePath(b.Name)), Class("ref-name"), g.Text(b.Name)),
g.If(b.Name == repo.DefaultBranch, Span(Class("badge"), g.Text("default"))),
g.If(isAdmin, Span(Class("ref-actions"),
branchRenameDetails(repo, b),
g.If(b.Name != repo.DefaultBranch, branchDeleteDetails(repo, b)),
)),
),
Div(Class("ref-meta-row"),
g.If(b.AuthorName != "", Span(Class("ref-author"), g.Text(b.AuthorName))),
@@ -226,10 +230,6 @@ func BranchList(cfg *config.Config, user *db.SessionUser, repo *db.Repo, branche
g.If(b.Subject != "", Span(Class("ref-subject"), g.Text(b.Subject))),
g.If(b.Date != "", Time(Class("ref-date"), DateTime(b.Date),
g.Text(util.FormatDateTime(b.Date)))),
g.If(isAdmin, Span(Class("ref-actions"),
branchRenameDetails(repo, b),
g.If(b.Name != repo.DefaultBranch, branchDeleteDetails(repo, b)),
)),
),
)
}),
@@ -317,6 +317,8 @@ func TagList(cfg *config.Config, user *db.SessionUser, repo *db.Repo, tags []git
A(Href("/"+repo.Name+"/tree/"+EscapePath(tag.Name)), Class("ref-name"), g.Text(tag.Name)),
g.If(releaseID != 0,
A(Href(releaseHref), Class("badge badge-release"), g.Text("release"))),
g.If(isAdmin, Span(Class("ref-actions"),
tagDeleteDetails(repo, tag, releaseID, releaseHref))),
),
Div(Class("ref-meta-row"),
g.If(tag.TaggerName != "", Span(Class("ref-author"), g.Text(tag.TaggerName))),
@@ -326,8 +328,6 @@ func TagList(cfg *config.Config, user *db.SessionUser, repo *db.Repo, tags []git
g.If(tag.Subject != "", Span(Class("ref-subject"), g.Text(tag.Subject))),
g.If(tag.Date != "", Time(Class("ref-date"), DateTime(tag.Date),
g.Text(util.FormatDateTime(tag.Date)))),
g.If(isAdmin, Span(Class("ref-actions"),
tagDeleteDetails(repo, tag, releaseID, releaseHref))),
),
)
}),
▾Minternal/web/views/repos_files.go
@@ -194,7 +194,7 @@ func RepoHome(cfg *config.Config, user *db.SessionUser, repo *db.Repo, entries [
A(Href("/"+repo.Name+"/new-file/"+EscapePath(repo.DefaultBranch)),
Class("btn btn-sm btn-primary"), g.Text("New file"))),
Details(Class("clone-popup-details"),
Summary(Class("btn btn-sm btn-primary"), g.Text("Clone")),
Summary(Class("btn btn-sm"), g.Text("Clone")),
Div(Class("clone-popup"),
Div(Class("clone-url-row"),
Span(Class("clone-url-label"), g.Text("HTTP")),
▾Ascripts/seed-design.sh
@@ -0,0 +1,443 @@
#!/usr/bin/env bash
# Seeds a fresh data dir with content for visual review of every view.
#
# scripts/seed-design.sh # creates ./data-test-design and seeds it
# DATA_DIR=data-test-design PORT=3999 ./hearthforge
#
# Admin login: admin / designdesign. Second user: alice / designdesign.
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
DATA_DIR=${DATA_DIR:-$ROOT/data-test-design}
PORT=${PORT:-3999}
BASE=http://localhost:$PORT
BIN=$ROOT/hearthforge
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
export GIT_AUTHOR_NAME="Alice Example" GIT_AUTHOR_EMAIL=alice@example.com
export GIT_COMMITTER_NAME="Alice Example" GIT_COMMITTER_EMAIL=alice@example.com
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
rm -rf "$DATA_DIR"
mkdir -p "$DATA_DIR/repos"
(cd "$ROOT" && go build -o hearthforge ./cmd/hearthforge)
DATA_DIR=$DATA_DIR ADMIN_PASSWORD=designdesign "$BIN" init
# ---------- git repos on disk ----------
bare() { git init -q --bare -b main "$DATA_DIR/repos/$1.git"; }
work() { rm -rf "$TMP/w"; git init -q -b main "$TMP/w"; cd "$TMP/w"; }
push() { git push -q "$DATA_DIR/repos/$1.git" --all && git push -q "$DATA_DIR/repos/$1.git" --tags; }
commit() { git add -A && GIT_AUTHOR_DATE="$2" GIT_COMMITTER_DATE="$2" git commit -q -m "$1"; }
# website: the main showcase repo.
bare website
work
mkdir -p src/components src/lib/deeply/nested/directory/structure docs assets
cat > README.md <<'EOF'
# Website
A small static site generator used as the demo project for **Hearthforge**.
## Features
- Renders Markdown to HTML with `goldmark`
- Live reload during development
- Zero configuration for the common case
## Quick start
```sh
git clone http://localhost:3999/website.git
cd website
make serve
```
| Command | Description |
|---------|-------------|
| `make build` | Build the site into `dist/` |
| `make serve` | Serve with live reload on port 8080 |
| `make clean` | Remove build output |
> [!NOTE]
> This is demo content. The commands do nothing.
### Task list
- [x] Markdown rendering
- [x] Syntax highlighting
- [ ] Search
- [ ] RSS feed
A very long line without any spaces to test overflow handling: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
![Logo](assets/logo.png)
1. First step
2. Second step
- nested item
- another nested item
3. Third step
Inline `code`, *emphasis*, **strong**, ~~strikethrough~~, and a [link](https://example.com).
---
Footer text.
EOF
cat > Makefile <<'EOF'
.PHONY: build serve clean
build:
go run ./cmd/site build
serve:
go run ./cmd/site serve --port 8080
clean:
rm -rf dist
EOF
cat > src/main.go <<'EOF'
// Package main is the entry point of the demo site generator.
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
)
// Page is one rendered page.
type Page struct {
Title string
Slug string
Content string
}
func main() {
out := flag.String("out", "dist", "output directory")
flag.Parse()
pages, err := loadPages("content")
if err != nil {
log.Fatal(err)
}
for _, p := range pages {
dst := filepath.Join(*out, p.Slug+".html")
if err := os.WriteFile(dst, []byte(render(p)), 0o644); err != nil {
log.Fatal(err)
}
fmt.Println("wrote", dst)
}
}
func loadPages(dir string) ([]Page, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var pages []Page
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
continue
}
b, err := os.ReadFile(filepath.Join(dir, e.Name()))
if err != nil {
return nil, err
}
pages = append(pages, Page{
Title: strings.TrimSuffix(e.Name(), ".md"),
Slug: strings.TrimSuffix(e.Name(), ".md"),
Content: string(b),
})
}
return pages, nil
}
func render(p Page) string {
return "<!doctype html><title>" + p.Title + "</title><main>" + p.Content + "</main>"
}
EOF
cat > src/components/header.js <<'EOF'
export function Header({ title, links = [] }) {
const nav = links.map((l) => `<a href="${l.href}">${l.label}</a>`).join("");
return `<header class="site-header"><h1>${title}</h1><nav>${nav}</nav></header>`;
}
// A very long line to test horizontal scrolling in the blob view: const veryLongVariableName = someFunction(anotherFunction(yetAnotherFunction(withManyArguments, andMoreArguments, andEvenMoreArguments, untilTheLineIsReallyLong)));
EOF
cat > src/components/styles.css <<'EOF'
:root {
--bg: #fff;
--fg: #111;
}
.site-header {
display: flex;
justify-content: space-between;
padding: 1rem;
}
EOF
echo 'package deep' > src/lib/deeply/nested/directory/structure/deep.go
cat > docs/CONTRIBUTING.md <<'EOF'
# Contributing
Send patches through the Patches tab. Sign your commits.
EOF
touch "docs/a file name with spaces and a rather long title that keeps going.md"
printf 'this-is-a-very-long-file-name-without-spaces-that-should-not-break-the-tree-layout-when-it-is-rendered.txt\n' > "this-is-a-very-long-file-name-without-spaces-that-should-not-break-the-tree-layout-when-it-is-rendered.txt"
cp "$ROOT/web/static/assets/favicon.svg" assets/logo.svg
cp "$ROOT/tests/data-test-13/avatars/1.png" assets/logo.png 2>/dev/null || cp "$ROOT/web/static/assets/favicon.svg" assets/logo.png
head -c 4096 /dev/urandom > assets/blob.bin
printf 'root = true\n\n[*]\nindent_style = tab\n' > .editorconfig
commit "Initial commit" "2025-11-02T10:00:00+01:00"
for i in $(seq 1 28); do
echo "line $i" >> docs/CHANGELOG.md
commit "Update changelog, entry $i
This is the body of commit $i. It explains the reasoning in more detail
and wraps over several lines so the commit view has something to show.
Signed-off-by: Alice Example <alice@example.com>" "2025-11-$(printf %02d $(( (i % 27) + 2 )))T1$((i % 10)):00:00+01:00"
done
cat > src/lib/render.go <<'EOF'
package lib
// Render turns Markdown into HTML.
func Render(md string) string {
return "<p>" + md + "</p>"
}
EOF
commit "Add render helper with a subject line that is deliberately very long so that truncation and wrapping in the commit list can be checked" "2026-01-05T09:30:00+01:00"
git tag -a v0.1.0 -m "First tagged release"
sed -i 's/<p>/<article>/; s/<\/p>/<\/article>/' src/lib/render.go
commit "render: use article element" "2026-02-10T14:00:00+01:00"
git tag v0.2.0
git tag -a v1.0.0-rc.1 -m "Release candidate"
git checkout -q -b feature/search
mkdir -p src/search
cat > src/search/index.go <<'EOF'
package search
// Index is a naive full text index.
type Index map[string][]string
// Add indexes one document.
func (i Index) Add(slug, text string) {
i[slug] = append(i[slug], text)
}
EOF
commit "search: add naive index" "2026-03-01T11:00:00+01:00"
git checkout -q main
git checkout -q -b fix/typo-in-readme
sed -i 's/Zero configuration/Zero-configuration/' README.md
commit "readme: hyphenate zero-configuration" "2026-03-03T08:00:00+01:00"
git checkout -q main
git checkout -q -b a-branch-with-a-very-long-name-that-should-be-truncated-in-the-selector
echo x > x.txt
commit "long branch" "2026-03-04T08:00:00+01:00"
git checkout -q main
push website
# Patch files from the branches.
git format-patch -q -1 feature/search --stdout > "$TMP/search.patch"
git format-patch -q -1 fix/typo-in-readme --stdout > "$TMP/typo.patch"
# A conflicting patch: edit a line that main changed after the base.
git checkout -q -b conflict HEAD~1
sed -i 's/Package main is/Package main was/' src/main.go
commit "main: change package doc" "2026-03-05T08:00:00+01:00"
git format-patch -q -1 --stdout > "$TMP/conflict.patch"
git checkout -q main
sed -i 's/Package main is/Package main IS/' src/main.go
commit "main: shout package doc" "2026-03-06T08:00:00+01:00"
push website
# Other repos.
bare empty-repo
bare the-quick-brown-fox-jumps-over-the-lazy-dog-repository-with-a-very-long-name
work
echo '# Long name' > README.md
commit "init" "2026-01-01T00:00:00+00:00"
push the-quick-brown-fox-jumps-over-the-lazy-dog-repository-with-a-very-long-name
bare no-readme
work
echo 'print("hi")' > main.py
commit "add main.py" "2026-01-02T00:00:00+00:00"
push no-readme
bare private-notes
work
printf '# Private\n\nSecret notes.\n' > README.md
commit "init" "2026-01-03T00:00:00+00:00"
push private-notes
for i in $(seq 1 12); do
bare "sample-$(printf %02d $i)"
work
printf '# sample-%02d\n' "$i" > README.md
commit "init" "2025-0$(( (i % 9) + 1 ))-1${i:0:1}T00:00:00+00:00"
push "sample-$(printf %02d $i)"
done
cd "$ROOT"
# ---------- server + forms ----------
DATA_DIR=$DATA_DIR PORT=$PORT "$BIN" >"$TMP/server.log" 2>&1 &
SERVER=$!
trap 'kill $SERVER 2>/dev/null; rm -rf "$TMP"' EXIT
for _ in $(seq 1 50); do curl -sf "$BASE/health" >/dev/null && break; sleep 0.2; done
J=$TMP/admin.jar
A=$TMP/alice.jar
post() { # jar path data...
local jar=$1 path=$2; shift 2
curl -sS -X POST -o /dev/null -w '%{http_code} %{redirect_url}\n' -b "$jar" -c "$jar" "$BASE$path" "$@" | sed "s|^|POST $path -> |"
}
post "$A" /register --data-urlencode username=alice --data-urlencode password=designdesign --data-urlencode password2=designdesign
post "$J" /login --data-urlencode username=admin --data-urlencode password=designdesign
post "$A" /login --data-urlencode username=alice --data-urlencode password=designdesign
# Repo settings.
post "$J" /website/settings --data-urlencode "description=Static site generator used as the Hearthforge demo project. This description is long enough to wrap onto a second line on narrow screens." --data-urlencode is_pinned=1 --data-urlencode allow_user_labels=1 --data-urlencode default_branch=main \
--data-urlencode $'issue_template=## Steps to reproduce\n\n1.\n2.\n\n## Expected\n\n## Actual\n' \
--data-urlencode $'patch_template=## Summary\n\n## Testing\n'
post "$J" /private-notes/settings --data-urlencode "description=Only visible to the admin." --data-urlencode is_private=1 --data-urlencode default_branch=main
post "$J" /no-readme/settings --data-urlencode "description=A repo without a README." --data-urlencode default_branch=main
post "$J" /sample-03/settings --data-urlencode is_pinned=1 --data-urlencode default_branch=main
# Labels.
for l in "bug #d73a4a" "enhancement #a2eeef" "documentation #0075ca" "good first issue #7057ff" "wontfix #ffffff" "help wanted #008672" "a-very-long-label-name-for-wrapping #e4e669" "dark #000000"; do
post "$J" /website/settings/labels --data-urlencode "name=${l% *}" --data-urlencode "color=${l##* }"
done
# CI secret (settings page only shows names).
post "$J" /website/settings/ci-secrets --data-urlencode name=DEPLOY_TOKEN --data-urlencode value=secret --data-urlencode "description=Token used by the deploy step"
# Issues.
post "$A" /website/issues --data-urlencode "title=Search returns no results for multi-word queries" --data-urlencode $'body=## Steps to reproduce\n\n1. Open the search box\n2. Type `hello world`\n\n## Expected\n\nResults containing both words.\n\n## Actual\n\nNothing. Console shows:\n\n```\nTypeError: cannot read properties of undefined\n at search (index.js:42)\n```\n\n- [x] reproduced on main\n- [ ] reproduced on v0.2.0\n\n| Browser | Result |\n|---|---|\n| Firefox | broken |\n| Chromium | broken |\n\n> Quoting a previous comment here.\n\nA very long unbreakable token: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' --data-urlencode label_ids=1 --data-urlencode label_ids=6
post "$J" /website/issues --data-urlencode "title=Add RSS feed" --data-urlencode "body=Would be nice to have an Atom or RSS feed for the blog section." --data-urlencode label_ids=2
post "$A" /website/issues --data-urlencode "title=Typo in CONTRIBUTING.md" --data-urlencode "body=" --data-urlencode label_ids=3 --data-urlencode label_ids=4
post "$J" /website/issues --data-urlencode "title=This issue has an extremely long title that goes on and on and on to test how the header wraps when there is a lot of text in it and it just keeps going" --data-urlencode "body=Body." --data-urlencode label_ids=7 --data-urlencode label_ids=8 --data-urlencode label_ids=5
post "$A" /website/issues --data-urlencode "title=Live reload does not pick up new files" --data-urlencode "body=New files under content/ are ignored until restart."
for i in $(seq 6 24); do
post "$J" /website/issues --data-urlencode "title=Filler issue number $i for pagination" --data-urlencode "body=Filler." >/dev/null
done
post "$J" /website/issues/3/close
post "$J" /website/issues/5/complete
post "$J" /website/issues/7/close
post "$J" /website/issues/8/complete
# Comments and reactions on issue 1.
post "$J" /website/issues/1/comments --data-urlencode $'body=Thanks for the report. I can reproduce this.\n\nThe tokenizer splits on whitespace but the query builder joins with `AND` incorrectly.'
post "$A" /website/issues/1/comments --data-urlencode "body=Happy to send a patch if you point me at the right file."
post "$J" /website/issues/1/comments --data-urlencode "body=src/search/index.go, function Query."
post "$A" /website/issues/1/comments/2/edit --data-urlencode "edit_body=Happy to send a patch if you point me at the right file. Edited: sent as patch 1."
post "$J" /website/issues/1/react --data-urlencode "emoji=👍"
post "$A" /website/issues/1/react --data-urlencode "emoji=👍"
post "$A" /website/issues/1/react --data-urlencode "emoji=🎉"
post "$J" /website/issues/1/react --data-urlencode "emoji=👀" --data-urlencode comment_id=2
post "$A" /website/issues/1/react --data-urlencode "emoji=❤️" --data-urlencode comment_id=2
post "$A" /website/issues/1/react --data-urlencode "emoji=🚀" --data-urlencode comment_id=2
post "$J" /website/issues/2/comments --data-urlencode "body=Short."
# Patches.
post "$A" /website/patches -F title="search: add naive index" -F $'description=## Summary\n\nAdds a first, naive full text index.\n\n## Testing\n\n`go test ./...`' -F patch_file=@"$TMP/search.patch" -F label_ids=2
post "$A" /website/patches -F title="readme: hyphenate zero-configuration" -F description="Tiny wording fix." -F patch_file=@"$TMP/typo.patch" -F label_ids=3
post "$A" /website/patches -F title="main: change package doc (conflicts with main)" -F description="This one should fail to apply." -F patch_file=@"$TMP/conflict.patch"
post "$A" /website/patches -F title="Closed without merging" -F description="" -F patch_file=@"$TMP/typo.patch"
post "$A" /website/patches -F title="A patch with a very long title that keeps going well past the width of the page in order to exercise wrapping in the header row" -F description="" -F patch_file=@"$TMP/search.patch"
for i in $(seq 6 22); do
post "$A" /website/patches -F title="Filler patch $i" -F description="" -F patch_file=@"$TMP/typo.patch" >/dev/null
done
post "$J" /website/patches/1/comments --data-urlencode "body=Looks good. Merging."
post "$A" /website/patches/1/react --data-urlencode "emoji=🚀"
VERSION=$(curl -s -b "$J" "$BASE/website/patches/2" | grep -o 'name="version" value="[^"]*"' | head -1 | sed 's/.*value="//; s/"//')
post "$J" /website/patches/2/merge --data-urlencode "version=$VERSION"
post "$J" /website/patches/4/close
# Releases.
printf 'binary asset content\n' > "$TMP/website-linux-amd64"
printf 'checksums\n' > "$TMP/SHA256SUMS.txt"
head -c 300000 /dev/urandom > "$TMP/website-1.0.0-rc.1-darwin-arm64.tar.gz"
post "$J" /website/releases -F name="v0.1.0 – First release" -F tag_name=v0.1.0 -F $'notes=## Highlights\n\n- Markdown rendering\n- Live reload\n\n## Breaking changes\n\nNone.\n\n## Full changelog\n\n'"$(for i in $(seq 1 12); do printf -- '- Change number %d with some explanatory text\n' "$i"; done)" -F include_source_code=on -F files=@"$TMP/website-linux-amd64" -F files=@"$TMP/SHA256SUMS.txt"
post "$J" /website/releases -F name="v0.2.0" -F tag_name=v0.2.0 -F notes="Small release. No assets."
post "$J" /website/releases -F name="1.0.0 release candidate 1 with a long name that should wrap somewhere sensible" -F tag_name=v1.0.0-rc.1 -F notes="" -F include_source_code=on -F files=@"$TMP/website-1.0.0-rc.1-darwin-arm64.tar.gz"
post "$J" /website/releases -F name="Nightly build" -F create_tag=on -F tag_name=nightly-2026-03-06 -F revision=main -F notes="Automated nightly build."
# SSH keys for the admin.
ssh-keygen -q -t ed25519 -N '' -C 'laptop' -f "$TMP/k1"
ssh-keygen -q -t ed25519 -N '' -C 'a-key-with-a-very-long-comment-for-wrapping-tests@workstation.example.internal' -f "$TMP/k2"
post "$J" /settings/ssh-keys --data-urlencode name="Laptop" --data-urlencode "public_key=$(cat "$TMP/k1.pub")"
post "$J" /settings/ssh-keys --data-urlencode name="Workstation with a considerably longer key name" --data-urlencode "public_key=$(cat "$TMP/k2.pub")"
# Avatar for alice.
curl -sS -o /dev/null -b "$A" -c "$A" "$BASE/settings/avatar" -F avatar=@"$ROOT/tests/data-test-13/avatars/1.png" || true
kill $SERVER; wait $SERVER 2>/dev/null || true
# ---------- rows the forms cannot create ----------
DB=$DATA_DIR/hearthforge.db
sqlite3 "$DB" <<'SQL'
INSERT INTO users (username, password_hash, created_at, is_pending, register_application) VALUES
('pending-bob', NULL, '2026-03-01T10:00:00Z', 1, 'I work with Alice on the website and would like to send patches.'),
('pending-carol', NULL, '2026-03-02T11:00:00Z', 1, 'A much longer application text. It goes on for a while to test how the queue item wraps. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'),
('pending-dave', NULL, '2026-03-03T12:00:00Z', 1, '');
INSERT INTO ci_run_counters (repo_id, last_run_id) SELECT id, 6 FROM repositories WHERE name='website';
INSERT INTO ci_runs (repo_id, triggered_by, trigger_source, commit_sha, commit_branch, status, started_at, finished_at, created_at, repo_run_id)
SELECT id, 1, 'push', 'a1b2c3d4e5f60718293a4b5c6d7e8f9012345678', 'main', 'success', '2026-03-06T08:00:05Z', '2026-03-06T08:03:41Z', '2026-03-06T08:00:00Z', 1 FROM repositories WHERE name='website';
INSERT INTO ci_runs (repo_id, triggered_by, trigger_source, commit_sha, commit_branch, status, started_at, finished_at, created_at, repo_run_id)
SELECT id, 1, 'push', 'b2c3d4e5f60718293a4b5c6d7e8f901234567890', 'feature/search', 'failure', '2026-03-06T09:00:05Z', '2026-03-06T09:01:12Z', '2026-03-06T09:00:00Z', 2 FROM repositories WHERE name='website';
INSERT INTO ci_runs (repo_id, triggered_by, trigger_source, commit_sha, commit_tag, status, started_at, finished_at, created_at, repo_run_id, variable_overrides)
SELECT id, 1, 'manual', 'c3d4e5f60718293a4b5c6d7e8f90123456789012', 'v0.2.0', 'cancelled', '2026-03-06T10:00:05Z', '2026-03-06T10:00:30Z', '2026-03-06T10:00:00Z', 3, '{"TARGET":"staging","VERBOSE":"1"}' FROM repositories WHERE name='website';
INSERT INTO ci_runs (repo_id, triggered_by, trigger_source, commit_sha, commit_branch, status, started_at, created_at, repo_run_id)
SELECT id, 1, 'push', 'd4e5f60718293a4b5c6d7e8f9012345678901234', 'main', 'running', '2026-03-06T11:00:05Z', '2026-03-06T11:00:00Z', 4 FROM repositories WHERE name='website';
INSERT INTO ci_runs (repo_id, triggered_by, trigger_source, commit_sha, commit_branch, status, created_at, repo_run_id)
SELECT id, 1, 'push', 'e5f60718293a4b5c6d7e8f90123456789012345a', 'main', 'queued', '2026-03-06T11:30:00Z', 5 FROM repositories WHERE name='website';
INSERT INTO ci_runs (repo_id, triggered_by, trigger_source, commit_sha, commit_branch, status, started_at, finished_at, created_at, repo_run_id)
SELECT id, 1, 'push', 'f60718293a4b5c6d7e8f90123456789012345abc', 'main', 'warning', '2026-03-05T08:00:05Z', '2026-03-05T08:02:00Z', '2026-03-05T08:00:00Z', 6 FROM repositories WHERE name='website';
INSERT INTO ci_steps (run_id, name, status, exit_code, started_at, finished_at, log) VALUES
(1, 'tools', 'success', 0, '2026-03-06T08:00:05Z', '2026-03-06T08:00:40Z', 'go: downloading mvdan.cc/gofumpt v0.8.0
go: downloading github.com/golangci/golangci-lint/v2 v2.1.0
installed'),
(1, 'test', 'success', 0, '2026-03-06T08:00:40Z', '2026-03-06T08:03:00Z', 'ok website/src 0.412s
ok website/src/lib 0.008s
ok website/src/search 0.031s'),
(1, 'build', 'success', 0, '2026-03-06T08:03:00Z', '2026-03-06T08:03:41Z', 'wrote dist/index.html
wrote dist/about.html
A very long log line that should scroll or wrap: ' || hex(randomblob(200))),
(2, 'tools', 'success', 0, '2026-03-06T09:00:05Z', '2026-03-06T09:00:30Z', 'installed'),
(2, 'test', 'failure', 1, '2026-03-06T09:00:30Z', '2026-03-06T09:01:12Z', '--- FAIL: TestIndex (0.00s)
index_test.go:12: expected 2 results, got 0
FAIL
FAIL website/src/search 0.004s'),
(2, 'build', 'skipped', NULL, NULL, NULL, ''),
(3, 'tools', 'success', 0, '2026-03-06T10:00:05Z', '2026-03-06T10:00:20Z', 'installed'),
(3, 'test', 'cancelled', NULL, '2026-03-06T10:00:20Z', '2026-03-06T10:00:30Z', 'ok website/src 0.4s'),
(3, 'build', 'skipped', NULL, NULL, NULL, ''),
(4, 'tools', 'success', 0, '2026-03-06T11:00:05Z', '2026-03-06T11:00:20Z', 'installed'),
(4, 'test', 'running', NULL, '2026-03-06T11:00:20Z', NULL, '=== RUN TestRender
'),
(4, 'build', 'pending', NULL, NULL, NULL, ''),
(5, 'tools', 'pending', NULL, NULL, NULL, ''),
(5, 'test', 'pending', NULL, NULL, NULL, ''),
(6, 'vulncheck', 'warning', 1, '2026-03-05T08:00:05Z', '2026-03-05T08:02:00Z', 'Vulnerability #1: GO-2026-0001
stdlib: something
');
INSERT INTO ci_artifacts (run_id, filename, size) VALUES (1, 'website', 8421376), (1, 'dist.tar.gz', 120333);
SQL
mkdir -p "$DATA_DIR/ci/artifacts/1"
printf 'artifact\n' > "$DATA_DIR/ci/artifacts/1/website"
printf 'artifact\n' > "$DATA_DIR/ci/artifacts/1/dist.tar.gz"
echo
echo "Seeded $DATA_DIR. Start with:"
echo " DATA_DIR=$DATA_DIR PORT=$PORT ./hearthforge"
echo "Login: admin / designdesign, alice / designdesign"
▾Mtests/e2e.issues.test.ts
@@ -274,7 +274,7 @@ describe('issue editing', () => {
const page = await adminCtx.newPage();
try {
await page.goto(issueUrl);
expect(await page.locator('time.edited-indicator').count()).toBeGreaterThan(0);
expect(await page.locator('.edited-indicator').count()).toBeGreaterThan(0);
} finally { await page.close(); }
});
▾Mtests/e2e.repos.test.ts
@@ -146,6 +146,18 @@ describe('repos', () => {
} finally { await page.close(); }
});
test('raw svg is served as an image under a sandbox policy', async () => {
const page = await adminCtx.newPage();
try {
const resp = await page.request.get(`${BASE}/my-repo/raw/main/logo.svg`);
expect(resp.status()).toBe(200);
expect(resp.headers()['content-type']).toContain('image/svg+xml');
expect(resp.headers()['content-security-policy']).toContain('sandbox;');
const txt = await page.request.get(`${BASE}/my-repo/raw/main/README.md`);
expect(txt.headers()['content-security-policy']).not.toContain('sandbox');
} finally { await page.close(); }
});
test('commit log shows initial commit', async () => {
const page = await adminCtx.newPage();
try {
▾Mtests/helpers.ts
@@ -251,6 +251,7 @@ export function seedRepo(name: string) {
git(['config', 'user.name', 'Test'], tmp);
writeFileSync(`${tmp}/README.md`, `# ${name}\n`);
writeFileSync(`${tmp}/index.js`, `console.log("hello");\n`);
writeFileSync(`${tmp}/logo.svg`, `<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"><rect width="8" height="8"/></svg>\n`);
git(['add', '-A'], tmp);
git(['commit', '-m', 'Initial commit'], tmp);
git(['push', 'origin', 'HEAD:main'], tmp);
▾Mweb/static/assets/css/base.css
@@ -25,9 +25,14 @@
padding: 0;
}
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
min-height: 100dvh;
min-width: 500px;
/* Tested floor. Narrower windows scroll instead of squeezing. */
min-width: 320px;
}
img,
video {
@@ -39,6 +44,7 @@
textarea,
select {
font: inherit;
color: inherit;
}
a {
color: inherit;
@@ -54,12 +60,20 @@
h5,
h6 {
font-weight: 600;
line-height: 1.2;
line-height: 1.25;
overflow-wrap: anywhere;
}
pre,
code {
font-family: var(--font-mono);
}
summary {
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
}
/* ============================================================
@@ -67,6 +81,8 @@
============================================================ */
@layer tokens {
:root {
color-scheme: light;
/* Colors — light */
--color-bg: #ffffff;
--color-bg-subtle: #f6f8fa;
@@ -89,6 +105,7 @@
--color-warning-bg: #fff8c5;
--color-merged: #8250df;
--color-merged-bg: #fbefff;
--color-info: #0b5cab;
/* Typography */
--font-sans:
@@ -116,6 +133,9 @@
--space-12: 3rem;
--space-16: 4rem;
/* Page gutter: shrinks with the viewport, no breakpoint needed. */
--gutter: clamp(var(--space-3), 3vw, var(--space-6));
/* Border radius */
--radius-sm: 4px;
--radius-md: 6px;
@@ -126,10 +146,12 @@
--shadow-sm: 0 1px 0 rgba(27, 31, 36, 0.04);
--shadow-md: 0 3px 6px rgba(140, 149, 159, 0.15);
--shadow-lg: 0 8px 24px rgba(140, 149, 159, 0.2);
--shadow-popup: 0 4px 16px rgba(0, 0, 0, 0.14);
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--color-bg: #0d1117;
--color-bg-subtle: #161b22;
--color-bg-inset: #1c2128;
@@ -151,11 +173,14 @@
--color-warning-bg: #2c2000;
--color-merged: #bc8cff;
--color-merged-bg: #1e0b3e;
--color-info: #388bfd;
--shadow-popup: 0 4px 16px rgba(0, 0, 0, 0.5);
}
}
/* Explicit theme overrides — applied via data-theme attribute set by cookie */
:root[data-theme="dark"] {
color-scheme: dark;
--color-bg: #0d1117;
--color-bg-subtle: #161b22;
--color-bg-inset: #1c2128;
@@ -175,8 +200,11 @@
--color-warning-bg: #2c2000;
--color-merged: #bc8cff;
--color-merged-bg: #1e0b3e;
--color-info: #388bfd;
--shadow-popup: 0 4px 16px rgba(0, 0, 0, 0.5);
}
:root[data-theme="light"] {
color-scheme: light;
--color-bg: #ffffff;
--color-bg-subtle: #f6f8fa;
--color-bg-inset: #eef0f2;
@@ -196,6 +224,8 @@
--color-warning-bg: #fff8c5;
--color-merged: #8250df;
--color-merged-bg: #fbefff;
--color-info: #0b5cab;
--shadow-popup: 0 4px 16px rgba(0, 0, 0, 0.14);
}
}
@@ -206,10 +236,12 @@
body {
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: 1.5;
color: var(--color-text);
background: var(--color-bg);
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: minmax(0, 1fr);
min-height: 100dvh;
}
@@ -224,10 +256,11 @@
.site-nav {
max-width: 1200px;
margin: 0 auto;
padding: var(--space-3) var(--space-4);
padding: var(--space-3) var(--gutter);
display: flex;
align-items: center;
gap: var(--space-4);
min-width: 0;
}
.site-logo {
@@ -238,6 +271,13 @@
font-weight: 700;
font-size: var(--text-lg);
color: var(--color-text);
min-width: 0;
}
.logo-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.logo-icon {
@@ -252,16 +292,21 @@
display: flex;
align-items: center;
gap: var(--space-3);
min-width: 0;
}
/* Signed in: the user name gives way first, the logo keeps its text. */
.site-nav:has(.nav-user) .site-logo {
flex-shrink: 0;
}
.site-main {
padding: var(--space-8) var(--space-4);
padding: clamp(var(--space-4), 4vw, var(--space-8)) var(--gutter);
min-width: 0;
}
.site-footer {
text-align: center;
padding: var(--space-6) var(--space-4);
padding: var(--space-6) var(--gutter);
font-size: var(--text-sm);
color: var(--color-text-muted);
border-top: 1px solid var(--color-border);
@@ -270,7 +315,7 @@
.container {
max-width: 1000px;
margin: 0 auto;
overflow-x: clip;
min-width: 0;
}
.container-narrow {
@@ -281,7 +326,7 @@
display: inline;
}
/* --- Two-step delete confirmation --- */
/* --- Two-step confirmation popups (details/summary) --- */
.confirm-details {
position: relative;
display: inline-block;
@@ -289,6 +334,9 @@
.confirm-details > summary {
list-style: none;
}
.confirm-details > summary::-webkit-details-marker {
display: none;
}
.confirm-popup {
position: absolute;
@@ -297,13 +345,22 @@
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-popup);
padding: var(--space-3) var(--space-4);
display: flex;
align-items: center;
gap: var(--space-3);
white-space: nowrap;
width: max-content;
max-width: min(24rem, calc(100vw - 2 * var(--gutter)));
z-index: 100;
font-size: var(--text-sm);
line-height: 1.6;
color: var(--color-text-muted);
text-align: left;
}
.confirm-popup > .btn,
.confirm-popup > .inline-form {
margin-left: var(--space-2);
vertical-align: middle;
}
.confirm-popup > .popup-form {
margin-top: var(--space-2);
}
}
▾Mweb/static/assets/css/code.css
@@ -4,15 +4,14 @@
width: 100%;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
border-collapse: collapse;
border-collapse: separate;
border-spacing: 0;
overflow: hidden;
margin-bottom: var(--space-6);
table-layout: auto;
}
.file-tree-row {
border-bottom: 1px solid var(--color-border-muted);
}
.file-tree-row:last-child {
border-bottom: none;
.file-tree-row + .file-tree-row td {
border-top: 1px solid var(--color-border-muted);
}
.file-tree-row:hover {
background: var(--color-bg-subtle);
@@ -25,13 +24,14 @@
vertical-align: middle;
}
.file-icon {
width: 36px;
width: 2.25rem;
padding-right: var(--space-1);
text-align: center;
line-height: 0;
}
.file-name {
width: 100%;
overflow-wrap: anywhere;
}
.file-name a {
color: var(--color-link);
@@ -73,39 +73,47 @@
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
gap: var(--space-2) var(--space-3);
padding: var(--space-2) var(--space-4);
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
margin-top: var(--space-4);
}
.file-blob-name {
font-size: var(--text-sm);
font-weight: 500;
flex: 1 1 8rem;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow-wrap: anywhere;
}
.file-blob-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
margin-left: auto;
}
.file-blob-actions-wrap {
flex-wrap: wrap;
/* Anchor the delete popup to the whole header, so it never runs off the
left edge when the button sits far from the right edge. */
.file-blob-header {
position: relative;
}
.file-blob-header .confirm-details {
position: static;
}
.file-blob-header .confirm-popup {
max-width: 100%;
}
.file-blob-body {
border: 1px solid var(--color-border);
border-top: none;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
overflow: auto;
min-width: 0;
}
.file-edit-textarea {
display: block;
width: 100%;
box-sizing: border-box;
font-family: var(--font-mono);
font-size: var(--text-sm);
line-height: 1.6;
@@ -114,12 +122,14 @@
color: var(--color-text);
border: none;
resize: vertical;
min-height: 400px;
min-height: 20rem;
tab-size: 4;
}
.file-edit-textarea:focus {
outline: 2px solid var(--color-accent);
outline-offset: -2px;
}
/* Blob line-number table */
.blob-table {
min-width: 100%;
@@ -132,7 +142,7 @@
}
.blob-ln {
width: 1%;
min-width: 48px;
min-width: 3rem;
text-align: right;
padding: 0 var(--space-3);
color: var(--color-text-muted);
@@ -151,17 +161,17 @@
.blob-code {
padding: 0 var(--space-4);
white-space: pre;
overflow: visible;
vertical-align: top;
tab-size: 4;
}
.blob-table tr:target {
background: color-mix(in srgb, #ddf4ff 60%, transparent);
background: color-mix(in srgb, var(--color-link) 12%, transparent);
}
.blob-table tr:target .blob-ln {
background: color-mix(in srgb, #ddf4ff 80%, transparent);
background: color-mix(in srgb, var(--color-link) 20%, transparent);
}
.file-download-notice {
padding: var(--space-8);
padding: clamp(var(--space-6), 6vw, var(--space-8)) var(--space-4);
text-align: center;
color: var(--color-text-muted);
}
@@ -198,7 +208,7 @@
flex-direction: column;
padding: var(--space-3) 0;
border-bottom: 1px solid var(--color-border-muted);
gap: var(--space-2);
gap: var(--space-1);
}
.commit-item:last-child {
border-bottom: none;
@@ -208,6 +218,7 @@
text-decoration: none;
font-size: var(--text-sm);
font-weight: 500;
overflow-wrap: anywhere;
}
.commit-subject:hover {
color: var(--color-link);
@@ -216,36 +227,29 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
flex-wrap: wrap;
gap: var(--space-1) var(--space-3);
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.commit-meta-right {
display: flex;
align-items: center;
gap: var(--space-3);
white-space: nowrap;
}
.commit-hash {
font-family: var(--font-mono);
color: var(--color-link);
text-decoration: none;
background: var(--color-bg-inset);
padding: 1px var(--space-2);
border-radius: var(--radius-sm);
}
.commit-hash:hover {
text-decoration: underline;
flex-wrap: wrap;
gap: var(--space-2);
margin-left: auto;
}
.mono {
font-family: var(--font-mono);
.commit-date {
white-space: nowrap;
}
/* --- Commit detail viewer --- */
/* --- Commit detail --- */
.commit-page-top {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.commit-card {
@@ -253,13 +257,15 @@
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
min-width: 0;
}
.commit-card-subject {
font-size: var(--text-xl);
font-weight: 600;
padding: var(--space-5) var(--space-6);
padding: var(--space-4) clamp(var(--space-4), 3vw, var(--space-6));
border-bottom: 1px solid var(--color-border);
line-height: 1.3;
overflow-wrap: anywhere;
}
.commit-card-subject-empty {
color: var(--color-text-muted);
@@ -268,74 +274,87 @@
font-size: var(--text-sm);
}
.commit-card-body {
padding: var(--space-4) var(--space-6);
padding: var(--space-4) clamp(var(--space-4), 3vw, var(--space-6));
font-family: inherit;
font-size: var(--text-sm);
color: var(--color-text-muted);
border-bottom: 1px solid var(--color-border);
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.commit-card-meta {
padding: var(--space-4) var(--space-6);
padding: var(--space-3) clamp(var(--space-4), 3vw, var(--space-6));
display: flex;
flex-direction: column;
gap: var(--space-2);
gap: var(--space-1);
background: var(--color-bg-subtle);
}
.commit-card-meta-row {
display: flex;
align-items: baseline;
gap: var(--space-3);
gap: var(--space-1) var(--space-3);
font-size: var(--text-sm);
flex-wrap: wrap;
}
.commit-meta-label {
font-weight: 600;
color: var(--color-text-muted);
min-width: 5rem;
min-width: 6rem;
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.commit-meta-value {
min-width: 0;
overflow-wrap: anywhere;
}
.commit-sha-full {
font-size: var(--text-xs);
background: var(--color-bg-inset);
padding: 2px var(--space-2);
border-radius: var(--radius-sm);
word-break: break-all;
}
.commit-stats-bar {
font-size: var(--text-sm);
color: var(--color-text-muted);
margin-bottom: var(--space-4);
margin-bottom: var(--space-3);
}
/* --- Commit layout (sidebar + diffs) --- */
/* --- Commit layout (file nav + diffs) ---
Flex wrap with two bases: the nav wants 14rem, the diffs want at least
34rem and take all spare space. When both do not fit on one row, the
diffs wrap below the nav and both stretch to full width. */
.commit-layout {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-5);
align-items: start;
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
align-items: flex-start;
container-type: inline-size;
}
.commit-file-nav {
flex: 1 1 14rem;
min-width: 0;
max-width: 100%;
}
.commit-layout:has(.file-nav-details:not([open])) .commit-file-nav {
flex: 0 1 auto;
}
.commit-diffs {
flex: 999 1 min(100%, 34rem);
min-width: 0;
}
@media (min-width: 900px) {
.commit-layout {
grid-template-columns: 220px 1fr;
}
.commit-layout:has(.file-nav-details:not([open])) {
grid-template-columns: auto 1fr;
/* The one width query: the file nav only sticks when it sits beside the
diffs. Stacked on top it would cover the diff while scrolling. */
@container (min-width: 50rem) {
.commit-file-nav {
position: sticky;
top: calc(3.5rem + var(--space-3));
max-height: calc(100vh - 3.5rem - var(--space-6));
overflow-y: auto;
}
}
/* --- File nav sidebar --- */
.commit-file-nav {
position: sticky;
top: calc(56px + var(--space-4));
max-height: calc(100vh - 140px);
overflow-y: auto;
}
.file-nav-details {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
@@ -346,7 +365,7 @@
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
@@ -354,15 +373,18 @@
user-select: none;
border-bottom: 1px solid var(--color-border);
}
.file-nav-toggle::-webkit-details-marker {
display: none;
}
.file-nav-details:not([open]) .file-nav-toggle {
border-bottom: none;
}
.file-nav-toggle-icon {
font-size: 1em;
font-size: 0.8em;
color: var(--color-text-muted);
transition: transform 0.15s;
}
.file-nav-details:not([open]) .file-nav-toggle-icon {
.file-nav-details:not([open]) > .file-nav-toggle .file-nav-toggle-icon {
transform: rotate(-90deg);
}
.file-nav-list {
@@ -377,46 +399,54 @@
font-size: var(--text-xs);
text-decoration: none;
color: var(--color-text);
border-radius: 0;
overflow: hidden;
}
.file-nav-item:hover {
background: var(--color-bg-inset);
}
.file-nav-status {
.file-nav-status,
.diff-status-badge {
flex-shrink: 0;
width: 16px;
height: 16px;
width: 1.125rem;
height: 1.125rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
font-size: 9px;
font-size: 10px;
font-weight: 700;
font-family: var(--font-mono);
}
.file-status-added {
.file-nav-status {
width: 1rem;
height: 1rem;
font-size: 9px;
}
.file-status-added,
.diff-status-added {
background: var(--color-success-bg);
color: var(--color-success);
}
.file-status-deleted {
.file-status-deleted,
.diff-status-deleted {
background: var(--color-danger-bg);
color: var(--color-danger);
}
.file-status-modified {
.file-status-modified,
.diff-status-modified {
background: var(--color-accent-bg);
color: var(--color-accent);
}
.file-status-renamed {
background: var(--color-merged-bg);
color: var(--color-merged);
}
.file-status-copied {
.file-status-renamed,
.file-status-copied,
.diff-status-renamed,
.diff-status-copied {
background: var(--color-merged-bg);
color: var(--color-merged);
}
.file-nav-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -454,8 +484,10 @@
list-style: none;
user-select: none;
}
.file-nav-dir > details:not([open]) .file-nav-toggle-icon {
.file-nav-dir-toggle::-webkit-details-marker {
display: none;
}
.file-nav-dir > details:not([open]) > .file-nav-dir-toggle .file-nav-toggle-icon {
transform: rotate(-90deg);
}
@@ -465,26 +497,29 @@
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
min-width: 0;
}
.diff-file-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
gap: var(--space-2) var(--space-3);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-subtle);
cursor: pointer;
list-style: none;
user-select: none;
flex-wrap: wrap;
}
.diff-file-header::-webkit-details-marker {
display: none;
}
.diff-file-header-left {
display: flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
flex: 1;
flex: 1 1 12rem;
}
.diff-file-header-right {
display: flex;
@@ -492,100 +527,54 @@
gap: var(--space-2);
flex-shrink: 0;
flex-wrap: wrap;
margin-left: auto;
}
.diff-file-toggle-icon {
font-size: 0.7em;
color: var(--color-text-muted);
flex-shrink: 0;
transition: transform 0.15s;
}
.diff-file:not([open]) .diff-file-toggle-icon {
transform: rotate(-90deg);
}
.diff-status-badge {
flex-shrink: 0;
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
font-size: 10px;
font-weight: 700;
font-family: var(--font-mono);
}
.diff-status-added {
background: var(--color-success-bg);
color: var(--color-success);
}
.diff-status-deleted {
background: var(--color-danger-bg);
color: var(--color-danger);
}
.diff-status-modified {
background: var(--color-accent-bg);
color: var(--color-accent);
}
.diff-status-renamed {
background: var(--color-merged-bg);
color: var(--color-merged);
}
.diff-status-copied {
background: var(--color-merged-bg);
color: var(--color-merged);
}
.diff-file-path {
font-size: var(--text-sm);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
overflow-wrap: anywhere;
}
.diff-rename-arrow {
font-size: var(--text-xs);
color: var(--color-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
overflow-wrap: anywhere;
}
.diff-stat-add {
color: var(--color-success);
.diff-stat-add,
.diff-stat-del,
.diff-stat-binary {
font-size: var(--text-sm);
font-family: var(--font-mono);
font-weight: 600;
white-space: nowrap;
}
.diff-stat-add {
color: var(--color-success);
}
.diff-stat-del {
color: var(--color-danger);
font-size: var(--text-sm);
font-family: var(--font-mono);
font-weight: 600;
}
.diff-stat-binary {
color: var(--color-text-muted);
font-size: var(--text-sm);
font-family: var(--font-mono);
font-weight: 400;
}
.btn-xs {
padding: 1px var(--space-2);
font-size: var(--text-xs);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-bg);
color: var(--color-text);
text-decoration: none;
cursor: pointer;
white-space: nowrap;
.diff-file-header .btn-xs {
font-family: var(--font-mono);
display: inline-flex;
align-items: center;
transition: background 0.1s;
}
.btn-xs:hover {
background: var(--color-bg-inset);
}
/* --- Diff file body --- */
.diff-binary-notice {
padding: var(--space-4) var(--space-6);
padding: var(--space-4);
font-size: var(--text-sm);
color: var(--color-text-muted);
font-style: italic;
@@ -594,20 +583,19 @@
border-top: 1px solid var(--color-border-muted);
overflow-x: auto;
}
.diff-file-body {
border-top: 1px solid var(--color-border);
}
.diff-hunk:first-child {
border-top: none;
}
.diff-hunk-header {
padding: var(--space-1) var(--space-4);
padding: var(--space-1) var(--space-3);
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: normal;
text-align: left;
background: color-mix(
in srgb,
var(--color-merged-bg) 50%,
var(--color-bg)
);
background: color-mix(in srgb, var(--color-merged-bg) 50%, var(--color-bg));
color: var(--color-merged);
border-bottom: 1px solid var(--color-border-muted);
white-space: pre;
@@ -620,10 +608,11 @@
font-family: var(--font-mono);
font-size: var(--text-xs);
line-height: 1.5;
tab-size: 4;
}
.diff-ln {
width: 44px;
min-width: 44px;
width: 2.75rem;
min-width: 2.75rem;
text-align: right;
padding: 0 var(--space-2);
color: var(--color-text-muted);
@@ -633,8 +622,8 @@
white-space: nowrap;
}
.diff-sign {
width: 18px;
min-width: 18px;
width: 1.125rem;
min-width: 1.125rem;
text-align: center;
padding: 0 var(--space-1);
user-select: none;
@@ -643,28 +632,17 @@
.diff-code {
padding: 0 var(--space-3);
white-space: pre;
overflow: visible;
vertical-align: top;
word-break: normal;
}
/* Row type backgrounds */
.diff-row-add {
background: color-mix(
in srgb,
var(--color-success-bg) 70%,
transparent
);
background: color-mix(in srgb, var(--color-success-bg) 70%, transparent);
}
.diff-row-del {
background: color-mix(in srgb, var(--color-danger-bg) 70%, transparent);
}
.diff-row-add .diff-ln,
.diff-row-add .diff-sign {
background: color-mix(
in srgb,
var(--color-success-bg) 90%,
transparent
);
background: color-mix(in srgb, var(--color-success-bg) 90%, transparent);
}
.diff-row-del .diff-ln,
.diff-row-del .diff-sign {
@@ -678,7 +656,6 @@
color: var(--color-danger);
font-weight: 700;
}
.diff-ctrl {
color: #f0f0f0;
background: #c0392b;
@@ -686,15 +663,6 @@
font-weight: bold;
padding: 0 1px;
}
/* Token colors and --code-bg/--code-fg come from /assets/code-theme.css,
generated from the chroma themes at runtime. */
@media (prefers-color-scheme: dark) {
.blob-table tr:target {
background: color-mix(in srgb, #1c3a52 60%, transparent);
}
.blob-table tr:target .blob-ln {
background: color-mix(in srgb, #1c3a52 80%, transparent);
}
}
}
▾Mweb/static/assets/css/components.css
@@ -3,12 +3,14 @@
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: 500;
line-height: 1.25;
text-decoration: none;
cursor: pointer;
background: var(--color-bg-subtle);
@@ -20,7 +22,6 @@
}
.btn:hover {
background: var(--color-bg-inset);
border-color: var(--color-border);
}
.btn:disabled {
opacity: 0.5;
@@ -32,19 +33,32 @@
color: #fff;
}
.btn-primary:hover {
background: color-mix(in srgb, var(--color-accent) 82%, #000);
background: color-mix(in srgb, var(--color-accent) 85%, #000);
}
.btn-secondary {
background: var(--color-bg-subtle);
border-color: var(--color-border);
}
.btn-danger {
background: var(--color-danger);
border-color: transparent;
color: #fff;
}
.btn-danger:hover {
background: color-mix(in srgb, var(--color-danger) 85%, #000);
}
.btn-sm {
padding: var(--space-1) var(--space-3);
font-size: 0.8125rem;
}
.btn-xs {
padding: 1px var(--space-2);
font-size: var(--text-xs);
border-radius: var(--radius-sm);
background: var(--color-bg);
}
.btn-block {
width: 100%;
justify-content: center;
}
/* --- Forms --- */
@@ -53,6 +67,7 @@
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
min-width: 0;
}
.form-group label,
.form-group .form-group-label {
@@ -60,9 +75,12 @@
font-weight: 500;
color: var(--color-text);
}
.form-group input:not([type="file"]):not([type="checkbox"]),
.form-group input:not([type="file"]):not([type="checkbox"]):not([type="radio"]),
.form-group textarea,
.form-group select {
.form-group select,
.form-input {
width: 100%;
min-width: 0;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
@@ -70,45 +88,58 @@
color: var(--color-text);
font-size: var(--text-sm);
}
.form-group input:not([type="file"]):not([type="checkbox"]):focus,
.form-group textarea:focus {
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus,
.form-input:focus,
.search-input:focus,
.branch-select:focus,
.repo-sort-select:focus,
.label-select:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
border-color: var(--color-accent);
}
.form-group input[type="file"] {
.form-group input[type="file"],
.form-input[type="file"] {
font-size: var(--text-sm);
color: var(--color-text);
padding: 0;
border: none;
background: none;
max-width: 100%;
}
.form-group textarea {
resize: vertical;
min-height: 120px;
min-height: 7.5rem;
}
.form-success {
background: var(--color-success-bg);
color: var(--color-success);
border: 1px solid var(--color-success);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
.form-textarea {
resize: vertical;
min-height: 4.5rem;
font-family: var(--font-mono);
font-size: var(--text-sm);
margin-bottom: var(--space-4);
}
.form-hint {
font-size: var(--text-sm);
color: var(--color-text-muted);
margin: 0;
}
.form-success,
.form-error {
background: var(--color-danger-bg);
color: var(--color-danger);
border: 1px solid var(--color-danger);
border: 1px solid;
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
margin-bottom: var(--space-4);
overflow-wrap: anywhere;
}
.form-success {
background: var(--color-success-bg);
color: var(--color-success);
}
.form-error {
background: var(--color-danger-bg);
color: var(--color-danger);
}
.form-actions {
display: flex;
@@ -120,8 +151,19 @@
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
padding: clamp(var(--space-4), 3vw, var(--space-6));
margin-top: var(--space-4);
min-width: 0;
}
.form-card > .text-muted {
font-size: var(--text-sm);
margin-bottom: var(--space-3);
}
.form-card .form-group input:not([type="file"]):not([type="checkbox"]):not([type="radio"]),
.form-card .form-group textarea,
.form-card .form-group select,
.form-card .form-input {
background: var(--color-bg);
}
.checkbox-label {
display: flex;
@@ -129,14 +171,25 @@
gap: var(--space-2);
cursor: pointer;
}
.checkbox-label input {
flex-shrink: 0;
}
.mono,
.monospace {
font-family: var(--font-mono);
}
/* --- Page header --- */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: var(--space-6);
gap: var(--space-4);
gap: var(--space-3) var(--space-4);
}
.page-header .page-title {
margin-bottom: 0;
}
.page-title {
font-size: var(--text-2xl);
@@ -156,11 +209,14 @@
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
.form-page {
max-width: 680px;
}
/* --- Auth --- */
.auth-container {
max-width: 400px;
margin: var(--space-12) auto;
margin: clamp(var(--space-4), 6vh, var(--space-12)) auto;
}
.auth-container .page-title {
margin-bottom: var(--space-6);
@@ -174,9 +230,20 @@
color: var(--color-text-muted);
margin-top: var(--space-4);
}
.passkey-btn {
display: none;
margin-top: var(--space-3);
.auth-divider {
display: flex;
align-items: center;
gap: var(--space-3);
margin: var(--space-4) 0;
color: var(--color-text-muted);
font-size: var(--text-sm);
}
.auth-divider::before,
.auth-divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--color-border);
}
/* --- Repo header --- */
@@ -191,27 +258,66 @@
}
.repo-title-row .page-title {
margin-bottom: 0;
}
.repo-header-title {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
overflow-wrap: anywhere;
}
.repo-description {
color: var(--color-text-muted);
margin-top: var(--space-2);
overflow-wrap: anywhere;
}
/* --- Repo nav bar (tabs) --- */
.repo-nav-bar {
border-bottom: 1px solid var(--color-border);
margin-bottom: var(--space-6);
}
.repo-header-meta {
.repo-tabs,
.patch-tab-nav {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-3);
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
}
.repo-tabs::-webkit-scrollbar,
.patch-tab-nav::-webkit-scrollbar {
display: none;
}
.repo-tab {
padding: var(--space-3) var(--space-3);
font-size: var(--text-sm);
text-decoration: none;
color: var(--color-text-muted);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
white-space: nowrap;
flex-shrink: 0;
transition: color 0.1s;
}
.repo-tab:hover {
color: var(--color-text);
}
.repo-tab.active {
color: var(--color-text);
font-weight: 600;
border-bottom-color: var(--color-accent);
}
.patch-tab-nav {
border-bottom: 1px solid var(--color-border);
margin-bottom: var(--space-6);
}
.icon {
font-style: normal;
.tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5em;
padding: 0 var(--space-1);
margin-left: var(--space-1);
font-size: var(--text-xs);
font-weight: 500;
line-height: 1.5;
background: var(--color-bg-inset);
border-radius: var(--radius-full);
color: var(--color-text-muted);
}
/* --- Branch selector --- */
@@ -219,31 +325,37 @@
display: inline-flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
}
.branch-selector-icon {
color: var(--color-text-muted);
font-size: var(--text-sm);
user-select: none;
}
.branch-select {
.branch-select,
.repo-sort-select,
.label-select {
padding: var(--space-1) var(--space-2);
padding-right: var(--space-5);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-subtle);
font-size: var(--text-sm);
font-family: var(--font-mono);
color: var(--color-text);
cursor: pointer;
appearance: auto;
max-width: 180px;
min-width: 0;
max-width: 100%;
}
.branch-select:hover {
border-color: var(--color-text-muted);
.branch-select {
font-family: var(--font-mono);
width: auto;
max-width: 14rem;
}
.branch-select:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
.branch-select:hover,
.repo-sort-select:hover,
.label-select:hover {
border-color: var(--color-text-muted);
}
/* toolbar row above file tree */
@@ -251,58 +363,56 @@
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
margin-bottom: var(--space-3);
}
.tree-toolbar-actions {
display: flex;
align-items: center;
gap: var(--space-2);
margin-left: auto;
}
/* commits-header flex row */
.commits-header {
display: flex;
align-items: center;
gap: var(--space-4);
gap: var(--space-3) var(--space-4);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.commits-header .section-title {
margin-bottom: 0;
border-bottom: none;
padding-bottom: 0;
}
/* --- Repo nav bar (tabs + private badge) --- */
.repo-nav-bar {
/* --- Repo list --- */
.search-row {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--color-border);
margin-bottom: var(--space-6);
gap: var(--space-4);
}
/* --- Repo tabs --- */
.repo-tabs {
display: flex;
gap: 0;
flex: 1;
overflow-x: auto;
overflow-y: hidden;
flex-wrap: wrap;
gap: var(--space-3);
margin-bottom: var(--space-5);
}
.repo-tab {
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
text-decoration: none;
color: var(--color-text-muted);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.1s;
.search-form {
flex: 1 1 14rem;
}
.repo-tab:hover {
color: var(--color-text);
.search-input-wrap {
display: flex;
gap: var(--space-2);
}
.repo-tab.active {
.search-input {
flex: 1;
min-width: 0;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg);
color: var(--color-text);
font-weight: 600;
border-bottom-color: var(--color-accent);
font-size: var(--text-sm);
}
/* --- Repo list --- */
.repo-list {
display: flex;
flex-direction: column;
@@ -312,11 +422,11 @@
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: var(--space-4) var(--space-5);
padding: var(--space-4) clamp(var(--space-4), 2vw, var(--space-5));
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-bg);
gap: var(--space-4);
gap: var(--space-2) var(--space-4);
transition: border-color 0.15s;
}
.repo-card:hover {
@@ -329,19 +439,25 @@
.repo-card-title {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
gap: var(--space-1) var(--space-2);
margin-bottom: var(--space-1);
}
.repo-name {
font-size: var(--text-lg);
font-weight: 600;
line-height: 1.3;
color: var(--color-link);
text-decoration: none;
word-break: break-all;
overflow-wrap: anywhere;
}
.repo-name:hover {
text-decoration: underline;
}
.repo-card .repo-description {
margin-top: 0;
font-size: var(--text-sm);
}
.repo-card-meta {
display: flex;
flex-direction: column;
@@ -350,9 +466,7 @@
font-size: var(--text-xs);
color: var(--color-text-muted);
white-space: nowrap;
}
.repo-date {
color: var(--color-text-muted);
padding-top: 0.3em;
}
/* --- Badges --- */
@@ -363,8 +477,17 @@
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid;
line-height: 1.5;
border: 1px solid var(--color-border);
text-decoration: none;
color: var(--color-text-muted);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
a.badge:hover {
text-decoration: underline;
}
.badge-private {
color: var(--color-warning);
@@ -381,64 +504,51 @@
border-color: var(--color-success);
background: var(--color-success-bg);
}
.badge-release:hover {
text-decoration: underline;
}
.issue-badge,
.patch-badge {
.patch-badge,
.sig-badge {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 500;
line-height: 1.25;
text-transform: capitalize;
white-space: nowrap;
}
.issue-badge.open,
.patch-badge.open {
.patch-badge.open,
.sig-badge.verified {
background: var(--color-success-bg);
color: var(--color-success);
}
.issue-badge.closed,
.patch-badge.closed {
.patch-badge.closed,
.sig-badge.unverified {
background: var(--color-danger-bg);
color: var(--color-danger);
}
.issue-badge.completed {
background: var(--color-merged-bg);
color: var(--color-merged);
}
.issue-badge.completed,
.patch-badge.merged {
background: var(--color-merged-bg);
color: var(--color-merged);
}
.sig-badge {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 500;
}
.sig-badge.verified {
background: var(--color-success-bg);
color: var(--color-success);
}
.sig-badge.unverified {
background: var(--color-danger-bg);
color: var(--color-danger);
}
/* --- Empty state --- */
.empty-state {
text-align: center;
padding: var(--space-16) var(--space-4);
padding: clamp(var(--space-8), 8vw, var(--space-16)) var(--space-2);
color: var(--color-text-muted);
}
.empty-state p {
margin-bottom: var(--space-4);
font-size: var(--text-lg);
}
.empty-state h2 {
margin-bottom: var(--space-3);
color: var(--color-text);
}
/* --- Breadcrumb --- */
.breadcrumb {
@@ -448,39 +558,47 @@
gap: var(--space-1);
font-size: var(--text-sm);
margin-bottom: var(--space-4);
overflow-wrap: anywhere;
}
.breadcrumb a {
color: var(--color-link);
text-decoration: none;
text-overflow: ellipsis;
overflow: hidden;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb-sep {
color: var(--color-text-muted);
}
.breadcrumb-sep,
.breadcrumb-current {
color: var(--color-text-muted);
text-overflow: ellipsis;
overflow: hidden;
}
/* --- Issue / Patch list --- */
/* --- List card (issues, patches, releases, refs, pipelines) --- */
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
padding: var(--space-3) var(--space-4);
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
gap: var(--space-4);
gap: var(--space-2) var(--space-4);
}
.list-header-tabs {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
}
.list-header-actions {
display: flex;
align-items: center;
gap: var(--space-2);
margin-left: auto;
}
.list-heading {
font-size: var(--text-lg);
font-weight: 600;
margin: 0;
}
.list-tab {
padding: var(--space-1) var(--space-3);
@@ -488,6 +606,7 @@
font-size: var(--text-sm);
text-decoration: none;
color: var(--color-text-muted);
white-space: nowrap;
}
.list-tab:hover {
background: var(--color-bg-inset);
@@ -498,40 +617,54 @@
color: var(--color-text);
font-weight: 600;
}
.issue-list {
.issue-list,
.ref-list {
border: 1px solid var(--color-border);
border-top: none;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
background: var(--color-bg);
}
.issue-item {
padding: var(--space-4) var(--space-4);
.issue-item,
.ref-item {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
min-width: 0;
}
.issue-item:last-child {
.issue-item:last-child,
.ref-item:last-child {
border-bottom: none;
}
.issue-main {
display: flex;
align-items: center;
align-items: baseline;
gap: var(--space-2);
margin-bottom: var(--space-1);
flex-wrap: wrap;
}
.issue-title {
font-weight: 500;
text-decoration: none;
color: var(--color-text);
flex: 1;
word-wrap: anywhere;
min-width: 0;
overflow-wrap: anywhere;
}
.issue-title:hover {
color: var(--color-link);
}
.issue-main .issue-number {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text-muted);
flex-shrink: 0;
}
.issue-meta {
font-size: var(--text-xs);
color: var(--color-text-muted);
display: flex;
gap: var(--space-3);
align-items: center;
flex-wrap: wrap;
gap: var(--space-1) var(--space-2);
padding-left: calc(8px + var(--space-2));
}
.issue-status-dot,
.patch-status-dot {
@@ -539,6 +672,7 @@
height: 8px;
border-radius: 50%;
flex-shrink: 0;
align-self: center;
}
.issue-status-dot.open,
.patch-status-dot.open {
@@ -548,27 +682,37 @@
.patch-status-dot.closed {
background: var(--color-danger);
}
.issue-status-dot.completed {
background: var(--color-merged);
}
.issue-status-dot.completed,
.patch-status-dot.merged {
background: var(--color-merged);
}
.issue-labels {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
margin-bottom: var(--space-2);
padding-left: calc(8px + var(--space-2));
}
/* --- Issue detail / Timeline --- */
/* --- Issue / patch detail header --- */
.issue-detail-header {
display: flex;
align-items: center;
align-items: flex-start;
flex-wrap: wrap;
gap: var(--space-3);
margin-bottom: var(--space-4);
gap: var(--space-2) var(--space-3);
margin-bottom: var(--space-3);
}
.issue-number {
.issue-detail-header .issue-number {
font-size: var(--text-2xl);
font-weight: 600;
line-height: 1.25;
color: var(--color-text-muted);
white-space: nowrap;
}
.issue-detail-header .issue-badge,
.issue-detail-header .patch-badge {
margin-top: 0.35em;
}
.issue-detail-title {
font-size: var(--text-2xl);
font-weight: 600;
@@ -577,44 +721,87 @@
.issue-detail-meta-actions {
margin-left: auto;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--space-2);
flex-shrink: 0;
padding-top: 0.2em;
}
.patch-author-meta {
/* Title edit (CSS-only toggle) */
.title-with-edit {
flex: 1 1 14rem;
min-width: 0;
display: flex;
align-items: center;
flex-direction: column;
gap: var(--space-1);
}
.title-edit-toggle {
display: none;
}
.title-edit-form-area {
display: none;
}
.title-edit-toggle:checked + .title-with-edit .issue-detail-title {
display: none;
}
.title-edit-open {
align-self: flex-start;
}
.title-edit-toggle:checked + .title-with-edit .title-edit-open {
display: none;
}
.title-edit-toggle:checked + .title-with-edit .title-edit-form-area {
display: flex;
flex: 1;
min-width: 0;
}
.title-edit-form {
display: flex;
flex-direction: column;
gap: var(--space-2);
font-size: var(--text-xs);
margin-top: calc(-1 * var(--space-4));
margin-bottom: var(--space-6);
flex: 1;
min-width: 0;
}
.patch-author-label {
color: var(--color-text-muted);
.title-edit-form .form-input {
font-size: var(--text-base);
}
.patch-author-identity {
font-family: monospace;
.title-edit-actions {
display: flex;
gap: var(--space-2);
}
/* --- Timeline (issue body, comments) --- */
.timeline-item {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
min-width: 0;
}
/* The card does not clip (the reaction picker must overflow it), so the
first and last child carry the inner radius themselves. */
.timeline-item > :first-child {
border-top-left-radius: calc(var(--radius-lg) - 1px);
border-top-right-radius: calc(var(--radius-lg) - 1px);
}
.timeline-item > :last-child {
border-bottom-left-radius: calc(var(--radius-lg) - 1px);
border-bottom-right-radius: calc(var(--radius-lg) - 1px);
}
.timeline-author {
background: var(--color-bg-subtle);
padding: var(--space-3) var(--space-4);
padding: var(--space-2) var(--space-4);
font-size: var(--text-sm);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-border);
overflow-wrap: anywhere;
}
.timeline-author time {
color: var(--color-text-muted);
font-size: var(--text-xs);
width: max-content;
white-space: nowrap;
}
.timeline-author-right {
margin-left: auto;
@@ -627,50 +814,6 @@
font-size: var(--text-xs);
cursor: default;
}
/* --- Avatars --- */
.avatar {
display: inline-flex;
border-radius: 4px;
overflow: hidden;
flex-shrink: 0;
vertical-align: middle;
}
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
color: transparent;
}
.avatar-placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
background: var(--color-bg-inset);
color: var(--color-text-muted);
font-weight: 600;
flex-shrink: 0;
vertical-align: middle;
}
.avatar-settings {
display: flex;
align-items: flex-start;
gap: var(--space-5);
}
.avatar-actions {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.nav-user {
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.timeline-body {
padding: var(--space-4);
}
@@ -681,10 +824,18 @@
font-size: var(--text-base);
border: none;
padding-bottom: 0;
margin-bottom: var(--space-3);
}
/* Edit <details> (summary only) in .timeline-author; form is a sibling shown via :has() */
.timeline-author-right .inline-edit-details {
border: none;
.timeline-item-new .form-group {
margin-bottom: var(--space-3);
}
/* Inline edit: the <summary> sits in the header, the form is a sibling. */
.inline-edit-details > summary {
list-style: none;
}
.inline-edit-details > summary::-webkit-details-marker {
display: none;
}
.inline-edit-details .when-open {
display: none;
@@ -705,76 +856,82 @@
.timeline-item:has(.inline-edit-details[open]) .timeline-body {
display: none;
}
/* Title edit in issue/patch header */
.issue-detail-header {
align-items: flex-start;
}
.title-with-edit {
flex: 1;
min-width: 16rem;
.inline-edit-form {
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.title-edit-toggle {
display: none;
gap: var(--space-3);
}
.title-edit-form-area {
display: none;
.inline-edit-form .form-group {
margin-bottom: 0;
}
.title-edit-toggle:checked + .title-with-edit .issue-detail-title {
display: none;
/* --- Avatars --- */
.avatar {
display: inline-flex;
border-radius: var(--radius-sm);
overflow: hidden;
flex-shrink: 0;
vertical-align: middle;
}
.title-edit-open {
align-self: flex-start;
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
color: transparent;
}
.title-edit-toggle:checked + .title-with-edit .title-edit-open {
display: none;
.avatar-placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
background: var(--color-bg-inset);
color: var(--color-text-muted);
font-weight: 600;
flex-shrink: 0;
vertical-align: middle;
}
.title-edit-toggle:checked + .title-with-edit .title-edit-form-area {
.avatar-settings {
display: flex;
flex: 1;
min-width: 0;
align-items: flex-start;
flex-wrap: wrap;
gap: var(--space-5);
}
.title-edit-form {
.avatar-actions {
display: flex;
flex-direction: column;
gap: var(--space-2);
flex: 1;
min-width: 0;
}
.title-edit-form .form-input {
gap: var(--space-3);
min-width: 0;
width: 100%;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg);
color: var(--color-text);
font-size: var(--text-sm);
}
.title-edit-form .form-input:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
border-color: var(--color-accent);
.avatar-actions .form-group {
margin-bottom: var(--space-3);
}
.title-edit-actions {
display: flex;
.nav-user {
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
color: var(--color-text-muted);
text-decoration: none;
min-width: 0;
}
.inline-edit-form {
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
.nav-user:hover {
color: var(--color-text);
}
/* The user name is the first thing to give way on a narrow screen. */
.nav-user span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* --- Reactions --- */
.reaction-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border-top: 1px solid var(--color-border-muted);
@@ -786,18 +943,22 @@
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
padding: 2px var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
background: var(--color-bg-subtle);
color: var(--color-text);
font-size: var(--text-sm);
line-height: 1.5;
cursor: pointer;
transition: background 0.1s;
}
.reaction-btn:hover {
background: var(--color-bg-inset);
}
.reaction-btn:disabled {
cursor: default;
}
.reaction-btn.reacted {
background: var(--color-accent-bg);
border-color: var(--color-accent);
@@ -809,33 +970,43 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
width: 1.75rem;
height: 1.75rem;
border: 1px dashed var(--color-border);
border-radius: var(--radius-full);
background: transparent;
cursor: pointer;
font-size: var(--text-base);
line-height: 1;
color: var(--color-text-muted);
list-style: none;
}
.reaction-add-btn::-webkit-details-marker {
display: none;
}
.reaction-add-btn:hover {
color: var(--color-text);
border-color: var(--color-text-muted);
}
.reaction-picker-dropdown {
position: absolute;
bottom: calc(100% + var(--space-2));
top: calc(100% + var(--space-2));
left: 0;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-popup);
padding: var(--space-2);
display: flex;
gap: var(--space-1);
z-index: 10;
width: max-content;
}
.reaction-picker-btn {
background: none;
border: none;
font-size: 1.25rem;
line-height: 1;
cursor: pointer;
padding: var(--space-1);
border-radius: var(--radius-sm);
@@ -844,116 +1015,66 @@
background: var(--color-bg-inset);
}
/* --- Patch subview tabs --- */
.patch-tab-nav {
display: flex;
border-bottom: 1px solid var(--color-border);
margin-bottom: var(--space-6);
}
.tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.4em;
padding: 0 var(--space-1);
margin-left: var(--space-1);
font-size: var(--text-xs);
font-weight: 500;
background: var(--color-bg-inset);
border-radius: 999px;
color: var(--color-text-muted);
}
/* --- Patch apply status --- */
.apply-status {
margin: var(--space-4) 0;
}
.apply-result {
display: flex;
align-items: flex-start;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-sm);
flex-direction: column;
min-width: 0;
}
.apply-result > div {
display: flex;
gap: var(--space-2);
align-items: center;
font-weight: 500;
}
.apply-clean {
background: var(--color-success-bg);
color: var(--color-success);
border: 1px solid var(--color-success);
}
.apply-conflict {
background: var(--color-danger-bg);
color: var(--color-danger);
border: 1px solid var(--color-danger);
}
.apply-checking,
.apply-unknown {
color: var(--color-text-muted);
font-style: italic;
font-size: var(--text-sm);
}
.apply-output {
font-family: var(--font-mono);
font-size: var(--text-xs);
white-space: pre-wrap;
margin-top: var(--space-2);
padding: var(--space-2);
background: rgba(0, 0, 0, 0.05);
overflow-wrap: anywhere;
padding: var(--space-2) var(--space-3);
background: color-mix(in srgb, currentColor 8%, transparent);
border-radius: var(--radius-sm);
width: 100%;
max-height: 20rem;
overflow: auto;
}
.patch-actions {
display: flex;
gap: var(--space-3);
margin: var(--space-4) 0;
flex-wrap: wrap;
}
.patch-card-title-row {
.upload-patch-form {
display: flex;
align-items: flex-start;
gap: var(--space-3);
justify-content: space-between;
margin-bottom: var(--space-3);
}
.patch-card-title-row .commit-card-subject {
margin-bottom: 0;
}
.patch-card-description {
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 1px solid var(--color-border-muted);
flex-direction: column;
gap: var(--space-2);
width: 100%;
}
/* --- Repo home layout --- */
.repo-home-layout {
display: grid;
gap: var(--space-8);
}
@media (min-width: 768px) {
.repo-home-layout {
grid-template-columns: 1fr 1fr;
}
}
/* --- README card --- */
.readme-section {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
min-width: 0;
}
.readme-header {
background: var(--color-bg-subtle);
padding: var(--space-3) var(--space-4);
padding: var(--space-2) var(--space-4);
font-size: var(--text-sm);
font-weight: 500;
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
}
/* --- Markdown body --- */
@@ -961,6 +1082,14 @@
font-size: var(--text-sm);
line-height: 1.6;
color: var(--color-text);
overflow-wrap: anywhere;
min-width: 0;
}
.markdown-body > :first-child {
margin-top: 0;
}
.markdown-body > :last-child {
margin-bottom: 0;
}
.markdown-body h1,
.markdown-body h2,
@@ -971,6 +1100,24 @@
margin-top: var(--space-6);
margin-bottom: var(--space-3);
}
.markdown-body h1 {
font-size: var(--text-2xl);
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border-muted);
}
.markdown-body h2 {
font-size: var(--text-xl);
padding-bottom: var(--space-1);
border-bottom: 1px solid var(--color-border-muted);
}
.markdown-body h3 {
font-size: var(--text-lg);
}
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
font-size: var(--text-base);
}
.markdown-body p {
margin-bottom: var(--space-4);
}
@@ -987,13 +1134,15 @@
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
padding: var(--space-3) var(--space-4);
overflow: auto;
margin-bottom: var(--space-4);
line-height: 1.5;
}
.markdown-body pre code {
background: none;
padding: 0;
font-size: var(--text-sm);
}
.markdown-body ul,
.markdown-body ol {
@@ -1009,12 +1158,17 @@
.markdown-body li {
margin-bottom: var(--space-1);
}
.markdown-body li > ul,
.markdown-body li > ol {
margin-top: var(--space-1);
margin-bottom: 0;
}
.markdown-body .task-list-item {
list-style: none;
}
.markdown-body .task-list-item-checkbox {
margin-right: var(--space-2);
vertical-align: middle;
}
.markdown-body blockquote {
border-left: 3px solid var(--color-border);
@@ -1023,7 +1177,10 @@
margin: var(--space-4) 0;
}
.markdown-body table {
width: 100%;
display: block;
width: max-content;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
margin-bottom: var(--space-4);
}
@@ -1054,111 +1211,137 @@
}
.readme-section .markdown-body,
.file-blob-body .markdown-body {
padding: var(--space-4);
padding: clamp(var(--space-4), 3vw, var(--space-6));
}
/* --- Code setup block --- */
/* --- Code setup block (empty repo) --- */
.code-setup {
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-4);
font-size: var(--text-sm);
margin-top: var(--space-4);
line-height: 1.6;
margin: var(--space-4) auto 0;
overflow: auto;
text-align: left;
}
.code-plain {
background: var(--color-bg-subtle);
padding: var(--space-4);
overflow: auto;
font-size: var(--text-sm);
line-height: 1.6;
max-width: 40rem;
}
/* --- Search row (search form + sort select) --- */
.search-row {
/* --- Passkeys, SSH keys, users (settings lists) --- */
.passkey-list {
display: flex;
flex-direction: column;
margin-bottom: var(--space-4);
}
.passkey-item {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-5);
padding: var(--space-2) 0;
border-bottom: 1px solid var(--color-border-muted);
font-size: var(--text-sm);
min-width: 0;
}
.search-form {
.passkey-item:last-child {
border-bottom: none;
}
.passkey-item form {
flex-shrink: 0;
margin-left: auto;
}
.passkey-date {
flex: 1;
color: var(--color-text-muted);
}
.ssh-key-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.repo-sort-select {
padding: var(--space-1) var(--space-2);
padding-right: var(--space-5);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-subtle);
font-size: var(--text-sm);
color: var(--color-text);
cursor: pointer;
appearance: auto;
.ssh-key-name {
font-weight: 500;
overflow-wrap: anywhere;
}
.repo-sort-select:hover {
border-color: var(--color-text-muted);
.ssh-key-fingerprint {
font-family: var(--font-mono);
font-size: var(--text-xs);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.repo-sort-select:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
.theme-options {
display: flex;
gap: var(--space-4);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.search-input-wrap {
.theme-option {
display: flex;
align-items: center;
gap: var(--space-2);
}
.search-input {
flex: 1;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg);
color: var(--color-text);
cursor: pointer;
font-size: var(--text-sm);
}
.search-input:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
border-color: var(--color-accent);
.settings-form {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.queue-bulk-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-bottom: var(--space-2);
}
.queue-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 32rem;
overflow-y: auto;
}
/* --- Issue close bar --- */
.issue-close-bar {
.queue-item {
display: flex;
justify-content: flex-end;
align-items: flex-start;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-2) var(--space-4);
padding: var(--space-3) 0;
border-top: 1px solid var(--color-border-muted);
margin-top: var(--space-2);
font-size: var(--text-sm);
}
/* --- Auth divider --- */
.auth-divider {
.queue-item-meta {
display: flex;
align-items: center;
gap: var(--space-3);
margin: var(--space-4) 0;
flex-direction: column;
gap: var(--space-1);
min-width: 0;
flex: 1 1 14rem;
}
.queue-item-header {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: var(--space-2);
}
.queue-item-date {
color: var(--color-text-muted);
font-size: var(--text-sm);
font-size: var(--text-xs);
}
.auth-divider::before,
.auth-divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--color-border);
.queue-item-answer {
margin: 0;
color: var(--color-text-muted);
font-size: var(--text-xs);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* --- Passkey UI in settings --- */
.passkey-actions {
.queue-item-actions {
display: flex;
flex-direction: column;
gap: var(--space-3);
align-items: flex-start;
margin-top: var(--space-3);
}
.passkey-status {
font-size: var(--text-sm);
gap: var(--space-2);
flex-shrink: 0;
margin-left: auto;
}
/* --- Danger zone --- */
@@ -1173,6 +1356,9 @@
border-color: var(--color-danger);
background: var(--color-danger-bg);
}
.danger-card .form-group input {
background: var(--color-bg);
}
.danger-item {
display: flex;
flex-direction: column;
@@ -1186,22 +1372,21 @@
padding-bottom: 0;
}
.danger-item + .danger-item {
border-top: 1px solid var(--color-danger);
border-top: 1px solid color-mix(in srgb, var(--color-danger) 40%, transparent);
}
.danger-item-info p {
margin: var(--space-1) 0 0;
font-size: var(--text-sm);
}
.danger-form {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-3);
gap: var(--space-2);
}
.danger-form .form-group {
width: min(360px, 100%);
}
.danger-form .form-group input {
width: 100%;
margin-bottom: 0;
}
.danger-form .confirm-popup {
left: 0;
@@ -1209,32 +1394,30 @@
top: auto;
bottom: calc(100% + var(--space-1));
}
.btn-danger {
background: var(--color-danger);
border-color: transparent;
color: #fff;
}
.btn-danger:hover {
background: color-mix(in srgb, var(--color-danger) 85%, #000);
}
/* --- Release list items --- */
.release-item-header {
display: flex;
gap: var(--space-3);
gap: var(--space-2) var(--space-3);
align-items: flex-start;
flex-wrap: wrap;
}
.release-item-main {
flex: 1;
flex: 1 1 14rem;
min-width: 0;
}
.release-item-title {
font-size: var(--text-lg);
font-weight: 700;
font-weight: 600;
line-height: 1.3;
text-decoration: none;
color: var(--color-text);
display: block;
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1) var(--space-2);
margin-bottom: var(--space-1);
overflow-wrap: anywhere;
}
.release-item-title:hover {
color: var(--color-link);
@@ -1242,25 +1425,27 @@
.release-item-meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
gap: var(--space-1) var(--space-3);
align-items: center;
font-size: var(--text-xs);
color: var(--color-text-muted);
min-width: 0;
}
.release-item-date {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-2);
gap: var(--space-1);
font-size: var(--text-xs);
color: var(--color-text-muted);
white-space: nowrap;
flex-shrink: 0;
margin-left: auto;
padding-top: 0.3em;
}
/* --- Release notes preview in list --- */
.release-notes-section {
margin-top: var(--space-3);
min-width: 0;
}
.release-notes-label {
font-size: var(--text-xs);
@@ -1273,9 +1458,10 @@
.notes-expand > summary {
list-style: none;
cursor: pointer;
outline: none;
}
.notes-expand > summary::-webkit-details-marker {
display: none;
}
.notes-preview {
max-height: calc(5 * 1.6em);
overflow: hidden;
@@ -1284,7 +1470,7 @@
font-size: var(--text-sm);
color: var(--color-text);
line-height: 1.6;
word-break: break-word;
overflow-wrap: anywhere;
margin-bottom: var(--space-1);
}
.notes-preview::after {
@@ -1301,8 +1487,8 @@
display: none;
}
.notes-full {
padding-top: var(--space-3);
padding-bottom: var(--space-1);
padding-top: var(--space-1);
padding-bottom: var(--space-2);
}
.notes-expand[open] {
display: flex;
@@ -1332,13 +1518,22 @@
/* --- Release detail --- */
.release-detail {
padding: var(--space-6) 0;
padding: var(--space-2) 0;
}
.release-header {
margin-bottom: var(--space-6);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--color-border);
}
.release-header .page-title {
margin-bottom: 0;
}
.release-header .release-item-date {
padding-top: 0.5em;
}
.release-notes {
margin-bottom: var(--space-6);
font-size: var(--text-base);
}
.release-assets {
margin-bottom: var(--space-6);
@@ -1356,7 +1551,8 @@
.asset-item {
display: flex;
align-items: center;
gap: var(--space-4);
flex-wrap: wrap;
gap: var(--space-1) var(--space-4);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
font-size: var(--text-sm);
@@ -1365,223 +1561,92 @@
border-bottom: none;
}
.asset-name {
flex: 1;
flex: 1 1 10rem;
color: var(--color-link);
text-decoration: none;
font-family: var(--font-mono);
font-size: var(--text-xs);
overflow-wrap: anywhere;
}
.asset-name:hover {
text-decoration: underline;
}
.asset-name-pending {
color: var(--color-text-muted);
cursor: default;
}
.asset-item-pending {
opacity: 0.6;
}
.asset-size,
.asset-meta {
color: var(--color-text-muted);
font-size: var(--text-xs);
white-space: nowrap;
}
.asset-size {
margin-left: auto;
}
.release-back {
margin-top: var(--space-4);
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
justify-content: space-between;
}
/* ============================================================
Pagination
============================================================ */
/* --- Pagination --- */
.commit-verify-hint {
margin-bottom: var(--space-4);
font-size: var(--text-xs);
color: var(--color-text-muted);
overflow-wrap: anywhere;
}
.commit-verify-hint code {
word-break: break-all;
}
.commit-cursor-nav {
display: flex;
justify-content: space-between;
border-top: 1px solid var(--color-border);
margin-top: var(--space-2);
padding: var(--space-4) 0 var(--space-2);
}
.commit-cursor-prev {
flex: 1;
}
.commit-cursor-next {
flex: 1;
text-align: right;
.commit-verify-hint a {
color: var(--color-link);
}
.commit-cursor-nav,
.pagination {
display: grid;
grid-template-columns: 1fr auto 1fr;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
border-top: 1px solid var(--color-border);
margin-top: var(--space-2);
padding: var(--space-4) 0 var(--space-2);
}
.pagination-prev {
justify-self: start;
}
.pagination-next {
justify-self: end;
}
.pagination-info {
justify-self: center;
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.pagination-btn {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: 500;
text-decoration: none;
color: var(--color-text);
background: var(--color-bg-subtle);
transition:
background 0.1s,
border-color 0.1s;
white-space: nowrap;
}
.pagination-btn:hover {
background: var(--color-bg-inset);
border-color: var(--color-border);
color: var(--color-text);
text-decoration: none;
}
/* Settings */
.theme-options {
display: flex;
gap: var(--space-4);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.theme-option {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
font-size: var(--text-sm);
}
.settings-form {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.passkey-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.passkey-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) 0;
border-bottom: 1px solid var(--color-border-muted);
font-size: var(--text-sm);
}
.passkey-item:last-child {
border-bottom: none;
}
.passkey-date {
flex: 1;
color: var(--color-text-muted);
}
.queue-bulk-actions {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.queue-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 32rem;
overflow-y: auto;
}
.queue-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-3) 0;
border-top: 1px solid var(--color-border-muted);
font-size: var(--text-sm);
}
.queue-item-meta {
display: flex;
flex-direction: column;
gap: var(--space-1);
min-width: 0;
}
.queue-item-header {
display: flex;
align-items: baseline;
gap: var(--space-2);
}
.queue-item-date {
color: var(--color-text-muted);
font-size: var(--text-xs);
}
.queue-item-answer {
margin: 0;
color: var(--color-text-muted);
font-size: var(--text-xs);
white-space: pre-wrap;
word-break: break-word;
}
.queue-item-actions {
display: flex;
gap: var(--space-2);
flex-shrink: 0;
.commit-cursor-prev,
.commit-cursor-next {
flex: 1;
}
.ssh-key-info {
.commit-cursor-next {
text-align: right;
}
.pagination-prev,
.pagination-next {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-1);
min-width: 0;
}
.ssh-key-name {
font-weight: 500;
.pagination-next {
text-align: right;
}
.ssh-key-fingerprint {
font-family: var(--font-mono);
font-size: var(--text-xs);
overflow: hidden;
text-overflow: ellipsis;
.pagination-info {
font-size: var(--text-sm);
color: var(--color-text-muted);
white-space: nowrap;
}
.form-textarea {
resize: vertical;
min-height: 4.5rem;
font-family: var(--font-mono);
.pagination-btn {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: 500;
text-decoration: none;
color: var(--color-text);
background: var(--color-bg-subtle);
white-space: nowrap;
}
.pagination-btn:hover {
background: var(--color-bg-inset);
}
/* Clone popup */
/* --- Clone popup --- */
.clone-popup-details {
position: relative;
display: inline-block;
@@ -1589,20 +1654,20 @@
.clone-popup-details > summary {
list-style: none;
}
.timeline-author-right .inline-edit-details > summary {
list-style: none;
.clone-popup-details > summary::-webkit-details-marker {
display: none;
}
.clone-popup {
position: absolute;
right: 1rem;
right: 0;
top: calc(100% + var(--space-2));
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-3);
min-width: 22rem;
width: min(26rem, calc(100vw - 2 * var(--gutter)));
z-index: 100;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
box-shadow: var(--shadow-popup);
display: flex;
flex-direction: column;
gap: var(--space-2);
@@ -1633,60 +1698,28 @@
min-width: 0;
}
/* User list */
.user-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-6);
}
.user-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) 0;
border-bottom: 1px solid var(--color-border-muted);
font-size: var(--text-sm);
}
.user-item:last-child {
border-bottom: none;
}
.user-name {
font-weight: 500;
flex: 1;
}
.user-date {
color: var(--color-text-muted);
font-size: var(--text-xs);
}
/* Labels */
/* --- Labels --- */
.label-badge {
display: inline-block;
padding: 0 var(--space-2);
border-radius: 2em;
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 500;
line-height: 1.6;
word-break: break-all;
}
.issue-labels {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
margin-bottom: var(--space-2);
overflow-wrap: anywhere;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.issue-labels-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) 0 var(--space-3);
padding: 0 0 var(--space-4);
}
.label-badge-wrap {
display: inline-flex;
align-items: center;
gap: var(--space-1);
gap: 2px;
}
.label-remove-form {
display: inline;
@@ -1695,28 +1728,27 @@
background: none;
border: none;
cursor: pointer;
padding: 0 2px;
padding: 0 var(--space-1);
font-size: var(--text-sm);
line-height: 1;
color: var(--color-text-muted);
opacity: 0.7;
}
.label-remove-btn:hover {
opacity: 1;
color: var(--color-danger);
}
.label-add-inline-form {
display: inline-flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
max-width: 100%;
}
.label-select {
font-size: var(--text-sm);
padding: var(--space-1) var(--space-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg);
color: var(--color-text);
height: 2rem;
padding-top: 0;
padding-bottom: 0;
background: var(--color-bg);
max-width: 14rem;
}
.label-checkbox-list {
display: flex;
@@ -1740,7 +1772,8 @@
.label-settings-item {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
padding: var(--space-2) 0;
border-bottom: 1px solid var(--color-border-muted);
font-size: var(--text-sm);
@@ -1748,6 +1781,9 @@
.label-settings-item:last-child {
border-bottom: none;
}
.label-settings-item form {
margin-left: auto;
}
.label-settings-swatch {
width: 12px;
height: 12px;
@@ -1756,30 +1792,21 @@
border: 1px solid rgba(0, 0, 0, 0.15);
}
.label-settings-name {
flex: 1;
flex: 1 1 8rem;
font-weight: 500;
overflow-wrap: anywhere;
}
.label-add-form {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
}
.label-name-input {
flex: 1;
min-width: 0;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg);
color: var(--color-text);
font-size: var(--text-sm);
flex: 1 1 10rem;
height: 2rem;
box-sizing: border-box;
}
.label-name-input:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
border-color: var(--color-accent);
padding-top: 0;
padding-bottom: 0;
}
.label-color-swatch-label {
flex-shrink: 0;
@@ -1790,6 +1817,7 @@
border-radius: 50%;
overflow: hidden;
line-height: 0;
border: 1px solid var(--color-border);
}
.label-color-input {
display: block;
@@ -1802,16 +1830,16 @@
padding: 0;
}
/* Label filter (details/summary popup) */
.list-header-actions {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
}
/* Label filter popup */
.label-filter {
position: relative;
}
.label-filter > summary {
list-style: none;
}
.label-filter > summary::-webkit-details-marker {
display: none;
}
.label-filter-popup {
position: absolute;
right: 0;
@@ -1821,8 +1849,9 @@
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-3);
min-width: 180px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
min-width: 12rem;
max-width: min(20rem, calc(100vw - 2 * var(--gutter)));
box-shadow: var(--shadow-popup);
}
.label-filter-list {
display: flex;
@@ -1844,29 +1873,11 @@
gap: var(--space-2);
}
/* --- list-heading (inside list-header) --- */
.list-heading {
font-size: var(--text-lg);
font-weight: 600;
margin: 0;
}
/* --- Branch / Tag ref list --- */
.ref-list {
border: 1px solid var(--color-border);
border-top: none;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.ref-item {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
}
.ref-item:last-child {
border-bottom: none;
}
.ref-name-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
margin-bottom: var(--space-1);
}
@@ -1875,6 +1886,7 @@
font-weight: 600;
color: var(--color-text);
text-decoration: none;
overflow-wrap: anywhere;
}
.ref-name:hover {
color: var(--color-link);
@@ -1883,7 +1895,7 @@
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
gap: var(--space-1) var(--space-3);
font-size: var(--text-xs);
color: var(--color-text-muted);
min-width: 0;
@@ -1891,7 +1903,8 @@
.ref-author {
white-space: nowrap;
}
.ref-hash {
.ref-hash,
.commit-hash {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-link);
@@ -1902,12 +1915,13 @@
white-space: nowrap;
flex-shrink: 0;
}
.ref-hash:hover {
.ref-hash:hover,
.commit-hash:hover {
text-decoration: underline;
}
.ref-subject {
flex: 1;
min-width: 60px;
flex: 1 1 10rem;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -1915,6 +1929,7 @@
.ref-date {
white-space: nowrap;
flex-shrink: 0;
margin-left: auto;
}
.ref-actions {
display: flex;
@@ -1923,99 +1938,85 @@
margin-left: auto;
flex-shrink: 0;
}
/* --- Inline confirm-details (for row-level actions) --- */
.confirm-details-inline {
position: relative;
display: inline-block;
}
/* --- Popup form (for new branch / new tag popups) --- */
.confirm-popup-form {
min-width: 260px;
min-width: min(16rem, calc(100vw - 2 * var(--gutter)));
}
.popup-form {
display: flex;
flex-direction: column;
gap: var(--space-3);
width: 100%;
color: var(--color-text);
}
.popup-form .form-group {
margin-bottom: 0;
}
.popup-form input[type="text"] {
width: 100%;
box-sizing: border-box;
min-width: 12rem;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg);
color: var(--color-text);
font-size: var(--text-sm);
}
/* --- Confirm warning (tag linked to release) --- */
.confirm-warning {
font-size: var(--text-xs);
color: var(--color-warning);
margin: 0 0 var(--space-2);
}
/* --- Tree toolbar actions group --- */
.tree-toolbar-actions {
display: flex;
align-items: center;
gap: var(--space-2);
margin: 0;
}
/* --- CI / Pipelines --- */
.ci-status-pill {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
padding: 2px var(--space-2);
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 600;
line-height: 1.4;
text-transform: uppercase;
letter-spacing: 0.03em;
vertical-align: middle;
color: #fff;
}
.ci-status-pending {
background: var(--color-border);
.ci-status-pending,
.ci-status-cancelled,
.ci-status-skipped {
background: var(--color-bg-inset);
color: var(--color-text-muted);
border: 1px solid var(--color-border);
padding: 1px calc(var(--space-2) - 1px);
}
.ci-status-queued {
background: #6e7781;
color: #fff;
}
.ci-status-running {
background: #0b5cab;
color: #fff;
background: var(--color-info);
}
.ci-status-success {
background: #1a7f37;
color: #fff;
background: var(--color-success);
}
.ci-status-failure {
background: #cf222e;
color: #fff;
background: var(--color-danger);
}
.ci-status-warning {
background: #bf8700;
color: #fff;
}
.ci-status-cancelled {
background: var(--color-border);
color: var(--color-text-muted);
}
.ci-status-skipped {
background: var(--color-border);
color: var(--color-text-muted);
background: var(--color-warning);
}
.ci-run-id {
font-weight: 600;
margin-left: var(--space-2);
}
.ci-sha {
font-family: var(--font-mono);
font-size: var(--text-xs);
background: var(--color-code-bg);
padding: 1px 4px;
border-radius: 3px;
background: var(--color-bg-inset);
padding: 1px var(--space-1);
border-radius: var(--radius-sm);
}
/* Header of the run detail page: title block left, actions right. */
.ci-run-header {
display: flex;
flex-wrap: wrap;
@@ -2024,20 +2025,29 @@
gap: var(--space-3);
margin-bottom: var(--space-5);
}
.ci-run-header > div:first-child {
min-width: 0;
flex: 1 1 16rem;
}
.release-detail-title {
font-size: var(--text-2xl);
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
margin-bottom: var(--space-2);
}
.ci-run-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
gap: var(--space-2);
margin-left: auto;
}
/* Manual trigger panel, shown when the config declares [variables]. */
.ci-run-popup {
display: block;
white-space: normal;
width: 20rem;
max-width: 90vw;
text-align: left;
width: min(20rem, calc(100vw - 2 * var(--gutter)));
color: var(--color-text);
}
.ci-run-vars {
max-height: 50vh;
@@ -2058,7 +2068,7 @@
}
.ci-step {
border: 1px solid var(--color-border);
border-radius: 6px;
border-radius: var(--radius-md);
margin-bottom: var(--space-2);
overflow: hidden;
}
@@ -2066,42 +2076,56 @@
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3);
padding: var(--space-2) var(--space-3);
cursor: pointer;
list-style: none;
background: var(--color-bg-secondary);
background: var(--color-bg-subtle);
font-size: var(--text-sm);
}
.ci-step-summary::-webkit-details-marker {
display: none;
}
.ci-step-summary::before {
content: "▸";
font-size: 0.7em;
color: var(--color-text-muted);
flex-shrink: 0;
transition: transform 0.15s;
}
.ci-step[open] > .ci-step-summary::before {
transform: rotate(90deg);
}
.ci-step-name {
font-weight: 600;
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
}
.ci-step-duration {
font-size: var(--text-xs);
white-space: nowrap;
}
.ci-step-log {
margin: 0;
padding: var(--space-3);
font-family: var(--font-mono);
font-size: var(--text-xs);
background: var(--color-code-bg);
overflow-x: auto;
line-height: 1.5;
background: var(--color-bg-inset);
border-top: 1px solid var(--color-border);
white-space: pre-wrap;
word-break: break-all;
max-height: 500px;
overflow-wrap: anywhere;
max-height: 32rem;
overflow-y: auto;
}
.ci-step-running-indicator {
padding: var(--space-3);
font-size: var(--text-sm);
}
.ci-step-running-indicator,
.ci-step-pending {
padding: var(--space-3);
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.ci-artifacts {
.ci-artifacts,
.ci-overrides {
margin-top: var(--space-4);
}
.ci-artifact-list {
@@ -2113,37 +2137,45 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-2) 0;
border-bottom: 1px solid var(--color-border);
font-size: var(--text-sm);
}
.ci-artifact-item:last-child {
border-bottom: none;
}
.ci-artifact-name {
font-weight: 500;
color: var(--color-link);
text-decoration: none;
overflow-wrap: anywhere;
}
.ci-artifact-name:hover {
text-decoration: underline;
}
.ci-artifact-size {
font-size: var(--text-xs);
font-family: var(--font-mono);
}
.ci-overrides {
margin-top: var(--space-4);
white-space: nowrap;
}
.ci-vars-list {
display: grid;
grid-template-columns: max-content 1fr;
grid-template-columns: max-content minmax(0, 1fr);
gap: var(--space-1) var(--space-4);
margin: 0;
font-size: var(--text-sm);
}
.ci-vars-list dt {
color: var(--color-text-muted);
}
.ci-vars-list dd {
margin: 0;
overflow-wrap: anywhere;
}
.ci-help {
margin-top: var(--space-4);
padding: var(--space-3) var(--space-4);
padding: var(--space-2) var(--space-3);
}
.ci-help-summary {
cursor: pointer;
@@ -2154,28 +2186,33 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
}
.ci-help-summary::-webkit-details-marker {
display: none;
}
.ci-help-download {
flex-shrink: 0;
margin-left: auto;
}
.ci-help-body {
padding: var(--space-2);
padding: var(--space-3) var(--space-2) var(--space-2);
border-top: 1px solid var(--color-border);
margin-top: 0.5rem;
margin-top: var(--space-2);
}
.ci-help-desc {
margin: var(--space-1) 0 var(--space-2);
margin: 0 0 var(--space-3);
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.ci-help-sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: var(--space-4);
}
.ci-help-section {
min-width: 0;
}
.ci-help-section-title {
font-size: var(--text-xs);
@@ -2187,7 +2224,7 @@
}
.ci-help-vars {
display: grid;
grid-template-columns: max-content 1fr;
grid-template-columns: max-content minmax(0, 1fr);
gap: 2px var(--space-3);
margin: 0;
font-size: var(--text-xs);
@@ -2208,9 +2245,9 @@
.ci-help-badge-code {
display: block;
font-size: var(--text-xs);
background: var(--color-code-bg);
background: var(--color-bg-inset);
padding: var(--space-1) var(--space-2);
border-radius: 3px;
border-radius: var(--radius-sm);
overflow-x: auto;
white-space: nowrap;
user-select: all;
▾Mweb/static/assets/css/utilities.css
@@ -2,46 +2,4 @@
.text-muted {
color: var(--color-text-muted);
}
.text-sm {
font-size: var(--text-sm);
}
.text-xs {
font-size: var(--text-xs);
}
.font-mono {
font-family: var(--font-mono);
}
.mt-1 {
margin-top: var(--space-1);
}
.mt-2 {
margin-top: var(--space-2);
}
.mt-4 {
margin-top: var(--space-4);
}
.mt-6 {
margin-top: var(--space-6);
}
.mb-4 {
margin-bottom: var(--space-4);
}
.flex {
display: flex;
}
.gap-2 {
gap: var(--space-2);
}
.gap-4 {
gap: var(--space-4);
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.hidden {
display: none;
}
}