add CSS build pipeline, avatar improvements, and various features

- Extract CSS to src/styles/main.css, compile with lightningcss via scripts/build-css.ts; add scripts/dev.ts to run server + CSS watcher together; update Containerfile for multi-stage CSS build
- Auto-create default avatar on GET /avatars/:id when none exists instead of 404
- Show avatars in issue and patch list views
- Add "completed" status for issues with a Completed tab and POST /:repo/issues/:number/complete endpoint
- Make git author name/email configurable via GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL env vars, used when merging patches
- Rewrite syntax highlighting output as a line-number table (blob-table) instead of plain <pre>
- Filter already-generated source archives from pending list in ReleaseDetail
- Conditionally show SSH clone URL based on SSH_DISABLED
- Add admin CTA to empty repo list

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
AuthorKonata <konata@posteo.jp>
Date
Commite5e41ae99d0e44d645c1e92fa71a91c31d1e6553
Parent673f60b
23 files changed, 2788 insertions(+), 1871 deletions(-)
▾M.gitignore
@@ -37,3 +37,6 @@ data/
# Test data
data-test/
# Generated CSS
public/assets/main.css
▾MContainerfile
@@ -1,3 +1,11 @@
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock* ./
COPY scripts ./scripts
COPY src/styles ./src/styles
RUN mkdir -p public/assets
RUN bun install --frozen-lockfile
FROM oven/bun:1 AS base
WORKDIR /app
@@ -7,10 +15,11 @@ RUN apt-get --update install -y --no-install-recommends \
# Copy public first so postinstall can write vendor files into it
COPY public ./public
COPY --from=builder /app/public/assets/* ./public/assets
# Install dependencies (postinstall vendors simplewebauthn + jxl-polyfill into public/)
# Install runtime dependencies (skip postinstall — builder stage handled it)
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile --production
RUN bun install --frozen-lockfile --production --ignore-scripts
# Copy source
COPY src ./src
▾MREADME.md
@@ -64,6 +64,9 @@ All settings are environment variables:
| `TRUSTED_PROXY` | `0` | Trust `X-Forwarded-For` |
| `RATE_LIMIT_DISABLED` | `0` | Set to `1` to disable rate limiting |
| `HIGHLIGHT_WORKERS` | `4` | Number of syntax highlighting workers* |
| `GIT_AUTHOR_NAME` | `OWNER_DISPLAY_NAME` | Author/committer name used when merging patches |
| `GIT_AUTHOR_EMAIL` | `{name}@{BASE_URL host}`| Author/committer email used when merging patches|
\* More workers mean more CPU cores can be used to parallelize highlighting of files.
Because of the language grammars, which can't be shared across workers, the memory usage per worker is quite high, at about 200MB.
So be careful when increasing this.
@@ -90,4 +93,4 @@ bun run test # Playwright E2E tests (don't use bun test, it doesn't res
- Use [git-bug](https://github.com/git-bug/git-bug) for issue tracking instead of custom implementation
- Issue labels
- Repository list reordering (e.g. last committed) and starring
- redirect image urls in readme
- redirect image urls in readme
▾Mbun.lock
@@ -28,6 +28,7 @@
"@types/argon2": "^0.15.4",
"@types/bun": "latest",
"@types/ssh2": "^1.15.5",
"lightningcss": "^1.32.0",
"playwright": "^1.58.2",
},
"peerDependencies": {
@@ -306,6 +307,30 @@
"kysely-bun-sqlite": ["kysely-bun-sqlite@0.4.0", "", { "dependencies": { "bun-types": "^1.1.31" }, "peerDependencies": { "kysely": "^0.28.2" } }, "sha512-2EkQE5sT4ewiw7IWfJsAkpxJ/QPVKXKO5sRYI/xjjJIJlECuOdtG+ssYM0twZJySrdrmuildNPFYVreyu1EdZg=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"linguist-languages": ["linguist-languages@9.3.1", "", {}, "sha512-Mum2sqg3MyhgKfpulFhKZMAK/1VnV6m9vCV8YQCSqWs+pbKouKn9EqRshZjVWUaJjl6NTTDcYJk/1+C02siXEQ=="],
"lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
▾Mpackage.json
@@ -2,14 +2,15 @@
"name": "hearthforge",
"module": "src/index.tsx",
"scripts": {
"dev": "bun run --watch src/index.tsx",
"dev": "bun scripts/dev.ts",
"start": "bun run src/index.tsx",
"css:build": "bun scripts/build-css.ts",
"db:init": "bun run src/db/init.ts",
"db:seed": "bun run src/db/seed.ts",
"test": "bun test tests/e2e.test.ts --timeout 60000",
"vendor:simplewebauthn": "bun build node_modules/@simplewebauthn/browser/esm/index.js --outfile public/assets/simplewebauthn-browser.js --format esm",
"vendor:jxl-polyfill": "cp node_modules/jxl-rs-polyfill/dist/auto.js public/assets/jxl-polyfill.js",
"postinstall": "bun run vendor:simplewebauthn && bun run vendor:jxl-polyfill",
"postinstall": "bun run vendor:simplewebauthn && bun run vendor:jxl-polyfill && bun run css:build",
"lint": "biome check src/",
"format": "biome check --write src/"
},
@@ -17,9 +18,12 @@
"private": true,
"devDependencies": {
"@biomejs/biome": "^2.4.7",
"@simplewebauthn/browser": "^13.3.0",
"@types/argon2": "^0.15.4",
"@types/bun": "latest",
"@types/ssh2": "^1.15.5",
"jxl-rs-polyfill": "^0.1.1",
"lightningcss": "^1.32.0",
"playwright": "^1.58.2"
},
"peerDependencies": {
@@ -29,13 +33,11 @@
"@elysiajs/static": "^1.4.7",
"@jsquash/jxl": "^1.3.0",
"@kitajs/html": "^4.2.13",
"@simplewebauthn/browser": "^13.3.0",
"@simplewebauthn/server": "^13.3.0",
"argon2": "^0.44.0",
"elysia": "^1.4.27",
"file-type": "^21.3.2",
"isomorphic-dompurify": "^3.3.0",
"jxl-rs-polyfill": "^0.1.1",
"kysely": "^0.28.12",
"kysely-bun-sqlite": "^0.4.0",
"linguist-languages": "^9.3.1",
▾Dpublic/assets/main.css
-1807
@@ -1,1807 +0,0 @@
/* View Transitions */
@view-transition {
navigation: auto;
}
.site-nav { view-transition-name: site-nav; }
.repo-header { view-transition-name: repo-header; }
.breadcrumb { view-transition-name: breadcrumb; }
/* ============================================================
@layer reset
============================================================ */
@layer reset {
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body { min-height: 100dvh; min-width: 500px; }
img, video { max-width: 100%; height: auto; }
button, input, textarea, select { font: inherit; }
a { color: inherit; }
ul, ol { list-style: none; }
h1, h2, h3, h4, h5, h6 { font-weight: 600; line-height: 1.2; }
pre, code { font-family: var(--font-mono); }
}
/* ============================================================
@layer tokens
============================================================ */
@layer tokens {
:root {
/* Colors — light */
--color-bg: #ffffff;
--color-bg-subtle: #f6f8fa;
--color-bg-inset: #eef0f2;
--color-border: #d0d7de;
--color-border-muted: #e4e7eb;
--color-text: #1f2328;
--color-text-muted: #656d76;
--color-link: #0969da;
--color-link-hover: #0550ae;
--color-accent: #fd7e14;
--color-accent-bg: #fff3cd;
--color-success: #1a7f37;
--color-success-bg: #dafbe1;
--color-danger: #cf222e;
--color-danger-bg: #ffebe9;
--color-warning: #9a6700;
--color-warning-bg: #fff8c5;
--color-merged: #8250df;
--color-merged-bg: #fbefff;
/* Typography */
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--space-16: 4rem;
/* Border radius */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-full: 9999px;
/* Shadows */
--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);
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0d1117;
--color-bg-subtle: #161b22;
--color-bg-inset: #1c2128;
--color-border: #30363d;
--color-border-muted: #21262d;
--color-text: #e6edf3;
--color-text-muted: #7d8590;
--color-link: #58a6ff;
--color-link-hover: #79c0ff;
--color-accent: #f0883e;
--color-accent-bg: #3d2f00;
--color-success: #3fb950;
--color-success-bg: #0d2b19;
--color-danger: #f85149;
--color-danger-bg: #2d1419;
--color-warning: #d29922;
--color-warning-bg: #2c2000;
--color-merged: #bc8cff;
--color-merged-bg: #1e0b3e;
}
}
/* Explicit theme overrides — applied via data-theme attribute set by cookie */
:root[data-theme="dark"] {
--color-bg: #0d1117;
--color-bg-subtle: #161b22;
--color-bg-inset: #1c2128;
--color-border: #30363d;
--color-border-muted: #21262d;
--color-text: #e6edf3;
--color-text-muted: #7d8590;
--color-link: #58a6ff;
--color-link-hover: #79c0ff;
--color-accent: #f0883e;
--color-accent-bg: #3d2f00;
--color-success: #3fb950;
--color-success-bg: #0d2b19;
--color-danger: #f85149;
--color-danger-bg: #2d1419;
--color-warning: #d29922;
--color-warning-bg: #2c2000;
--color-merged: #bc8cff;
--color-merged-bg: #1e0b3e;
}
:root[data-theme="light"] {
--color-bg: #ffffff;
--color-bg-subtle: #f6f8fa;
--color-bg-inset: #eef0f2;
--color-border: #d0d7de;
--color-border-muted: #e4e7eb;
--color-text: #1f2328;
--color-text-muted: #656d76;
--color-link: #0969da;
--color-link-hover: #0550ae;
--color-accent: #fd7e14;
--color-accent-bg: #fff3cd;
--color-success: #1a7f37;
--color-success-bg: #dafbe1;
--color-danger: #cf222e;
--color-danger-bg: #ffebe9;
--color-warning: #9a6700;
--color-warning-bg: #fff8c5;
--color-merged: #8250df;
--color-merged-bg: #fbefff;
}
}
/* ============================================================
@layer layout
============================================================ */
@layer layout {
body {
font-family: var(--font-sans);
font-size: var(--text-base);
color: var(--color-text);
background: var(--color-bg);
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100dvh;
}
.site-header {
background: var(--color-bg-subtle);
border-bottom: 1px solid var(--color-border);
position: sticky;
top: 0;
z-index: 100;
}
.site-nav {
max-width: 1200px;
margin: 0 auto;
padding: var(--space-3) var(--space-4);
display: flex;
align-items: center;
gap: var(--space-4);
}
.site-logo {
display: flex;
align-items: center;
gap: var(--space-2);
text-decoration: none;
font-weight: 700;
font-size: var(--text-lg);
color: var(--color-text);
}
.logo-icon { width: 1.6em; height: 1.6em; display: block; flex-shrink: 0; }
.nav-links {
margin-left: auto;
display: flex;
align-items: center;
gap: var(--space-3);
}
.nav-user {
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.site-main {
padding: var(--space-8) var(--space-4);
min-width: 0;
}
.site-footer {
text-align: center;
padding: var(--space-6) var(--space-4);
font-size: var(--text-sm);
color: var(--color-text-muted);
border-top: 1px solid var(--color-border);
}
.container {
max-width: 1000px;
margin: 0 auto;
overflow-x: clip;
}
.container-narrow {
max-width: 680px;
}
.inline-form {
display: inline;
}
/* --- Two-step delete confirmation --- */
.confirm-details { position: relative; display: inline-block; }
.confirm-details > summary { list-style: none; }
.confirm-details > summary::-webkit-details-marker { display: none; }
.confirm-popup {
position: absolute;
right: 0;
top: calc(100% + var(--space-1));
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
display: flex;
align-items: center;
gap: var(--space-3);
white-space: nowrap;
z-index: 100;
font-size: var(--text-sm);
color: var(--color-text-muted);
}
}
/* ============================================================
@layer components
============================================================ */
@layer components {
/* --- Buttons --- */
.btn {
display: inline-flex;
align-items: 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;
text-decoration: none;
cursor: pointer;
background: var(--color-bg-subtle);
color: var(--color-text);
transition: background 0.1s, border-color 0.1s;
white-space: nowrap;
}
.btn:hover { background: var(--color-bg-inset); border-color: var(--color-border); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary {
background: var(--color-accent);
border-color: transparent;
color: #fff;
}
.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 82%, #000); }
.btn-secondary {
background: var(--color-bg-subtle);
border-color: var(--color-border);
}
.btn-ghost {
background: transparent;
border-color: transparent;
}
.btn-ghost:hover { background: var(--color-bg-inset); }
/* Keep ghost transparent even when combined with size variants */
.btn-ghost.btn-xs,
.btn-ghost.btn-sm { background: transparent; border-color: transparent; }
.btn-ghost.btn-xs:hover,
.btn-ghost.btn-sm:hover { background: var(--color-bg-inset); }
.btn-sm { padding: var(--space-1) var(--space-3); font-size: var(--text-xs); }
.btn-block { width: 100%; justify-content: center; }
/* --- Forms --- */
.form-group {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.form-group label {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text);
}
.form-group input:not([type="file"]):not([type="checkbox"]),
.form-group textarea,
.form-group select {
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);
}
.form-group input:not([type="file"]):not([type="checkbox"]):focus,
.form-group textarea:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
border-color: var(--color-accent);
}
.form-group input[type="file"] {
font-size: var(--text-sm);
color: var(--color-text);
padding: 0;
border: none;
background: none;
}
.form-group textarea { resize: vertical; min-height: 120px; }
.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);
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-error {
background: var(--color-danger-bg);
color: var(--color-danger);
border: 1px solid var(--color-danger);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
margin-bottom: var(--space-4);
}
.form-actions {
display: flex;
gap: var(--space-3);
align-items: center;
flex-wrap: wrap;
}
.form-card {
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
margin-top: var(--space-4);
}
.checkbox-label {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
}
/* --- Auth --- */
.auth-container {
max-width: 400px;
margin: var(--space-12) auto;
}
.auth-container .page-title { margin-bottom: var(--space-6); }
.auth-form { margin-bottom: var(--space-4); }
.auth-footer {
text-align: center;
font-size: var(--text-sm);
color: var(--color-text-muted);
margin-top: var(--space-4);
}
.passkey-btn { display: none; margin-top: var(--space-3); }
/* --- Page header --- */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-6);
gap: var(--space-4);
}
.page-title {
font-size: var(--text-2xl);
color: var(--color-text);
margin-bottom: var(--space-4);
}
.page-title a { text-decoration: none; color: var(--color-link); }
.page-title a:hover { text-decoration: underline; }
.section-title {
font-size: var(--text-lg);
margin-bottom: var(--space-4);
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* --- Repo header --- */
.repo-header {
margin-bottom: var(--space-2);
}
.repo-title-row {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
}
.repo-title-row .page-title { margin-bottom: 0; }
.repo-header-title {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
}
.repo-description {
color: var(--color-text-muted);
margin-top: var(--space-2);
}
.repo-header-meta {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-3);
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.icon { font-style: normal; }
/* --- Branch selector --- */
.branch-selector {
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.branch-selector-icon {
color: var(--color-text-muted);
font-size: var(--text-sm);
user-select: none;
}
.branch-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;
}
.branch-select:hover { border-color: var(--color-text-muted); }
.branch-select:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }
/* toolbar row above file tree */
.tree-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-3);
}
/* commits-header flex row */
.commits-header {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.commits-header .section-title { margin-bottom: 0; }
/* --- Repo nav bar (tabs + private badge) --- */
.repo-nav-bar {
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;
}
.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;
}
.repo-tab:hover { color: var(--color-text); }
.repo-tab.active {
color: var(--color-text);
font-weight: 600;
border-bottom-color: var(--color-accent);
}
/* Align active indicator with nav-bar border */
.repo-tab { margin-bottom: -1px; }
/* --- Repo list --- */
.repo-list { display: flex; flex-direction: column; gap: var(--space-2); }
.repo-card {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: var(--space-4) var(--space-5);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-bg);
gap: var(--space-4);
transition: border-color 0.15s;
}
.repo-card:hover { border-color: var(--color-accent); }
.repo-card-main { flex: 1; min-width: 0; }
.repo-card-title {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
margin-bottom: var(--space-1);
}
.repo-name {
font-size: var(--text-lg);
font-weight: 600;
color: var(--color-link);
text-decoration: none;
}
.repo-name:hover { text-decoration: underline; }
.repo-card-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-1);
font-size: var(--text-xs);
color: var(--color-text-muted);
white-space: nowrap;
}
.repo-date { color: var(--color-text-muted); }
/* --- Badges --- */
.badge {
display: inline-flex;
align-items: center;
padding: 1px var(--space-2);
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid;
}
.badge-private {
color: var(--color-warning);
border-color: var(--color-warning);
background: var(--color-warning-bg);
}
.issue-badge, .patch-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;
text-transform: capitalize;
}
.issue-badge.open, .patch-badge.open {
background: var(--color-success-bg);
color: var(--color-success);
}
.issue-badge.closed, .patch-badge.closed {
background: var(--color-danger-bg);
color: var(--color-danger);
}
.patch-badge.merged {
background: var(--color-merged-bg);
color: var(--color-merged);
}
/* --- Empty state --- */
.empty-state {
text-align: center;
padding: var(--space-16) var(--space-4);
color: var(--color-text-muted);
}
.empty-state p { margin-bottom: var(--space-4); font-size: var(--text-lg); }
/* --- Breadcrumb --- */
.breadcrumb {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1);
font-size: var(--text-sm);
margin-bottom: var(--space-4);
}
.breadcrumb a { color: var(--color-link); text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
.breadcrumb-sep { color: var(--color-text-muted); }
.breadcrumb-current { color: var(--color-text-muted); }
/* --- File tree --- */
.file-tree {
width: 100%;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
border-collapse: collapse;
overflow: hidden;
margin-bottom: var(--space-6);
}
.file-tree-row {
border-bottom: 1px solid var(--color-border-muted);
}
.file-tree-row:last-child { border-bottom: none; }
.file-tree-row:hover { background: var(--color-bg-subtle); }
.file-icon, .file-name, .file-size {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
vertical-align: middle;
}
.file-icon {
width: 36px;
padding-right: var(--space-1);
text-align: center;
line-height: 0;
}
.file-name { width: 100%; }
.file-name a { color: var(--color-link); text-decoration: none; }
.file-name a:hover { text-decoration: underline; }
.file-name a.file-name-dir { font-weight: 500; }
.file-size {
text-align: right;
color: var(--color-text-muted);
font-size: var(--text-xs);
white-space: nowrap;
padding-right: var(--space-4);
}
.file-icon-dir { color: var(--color-link); }
.file-icon-file { color: var(--color-text-muted); }
.tree-icon { display: inline-block; vertical-align: text-bottom; }
.file-tree-row-up .file-name a { color: var(--color-text-muted); }
.file-tree-row-up .file-name a:hover { color: var(--color-link); }
/* --- File blob --- */
.file-blob-header {
display: flex;
align-items: center;
justify-content: space-between;
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;
}
.file-blob-name { font-size: var(--text-sm); font-weight: 500; }
.file-blob-body {
border: 1px solid var(--color-border);
border-top: none;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
overflow: auto;
}
.file-blob-body .shiki-wrapper pre {
margin: 0;
padding: var(--space-4);
font-size: var(--text-sm);
line-height: 1.6;
min-width: max-content;
}
.file-download-notice {
padding: var(--space-8);
text-align: center;
color: var(--color-text-muted);
}
.file-download-notice p { margin-bottom: var(--space-4); }
/* --- Commit list --- */
.commit-list { display: flex; flex-direction: column; }
.commit-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: var(--space-3) 0;
border-bottom: 1px solid var(--color-border-muted);
gap: var(--space-4);
flex-wrap: wrap;
}
.commit-item:last-child { border-bottom: none; }
.commit-main { flex: 1; min-width: 0; }
.commit-subject {
color: var(--color-text);
text-decoration: none;
font-size: var(--text-sm);
font-weight: 500;
}
.commit-subject:hover { color: var(--color-link); }
.commit-meta {
display: flex;
align-items: center;
gap: var(--space-3);
font-size: var(--text-xs);
color: var(--color-text-muted);
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; }
.mono { font-family: var(--font-mono); }
/* --- Commit detail viewer --- */
.commit-page-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-4);
}
.commit-card {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
}
.commit-card-subject {
font-size: var(--text-xl);
font-weight: 600;
padding: var(--space-5) var(--space-6);
border-bottom: 1px solid var(--color-border);
line-height: 1.3;
}
.commit-card-body {
padding: var(--space-4) 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;
}
.commit-card-meta {
padding: var(--space-4) var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-2);
background: var(--color-bg-subtle);
}
.commit-card-meta-row {
display: flex;
align-items: baseline;
gap: 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;
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.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);
}
/* --- Commit layout (sidebar + diffs) --- */
.commit-layout {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-5);
align-items: start;
}
.commit-diffs { 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; }
}
/* --- 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);
background: var(--color-bg-subtle);
overflow: hidden;
}
.file-nav-toggle {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
list-style: none;
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;
transition: transform 0.15s;
}
.file-nav-details:not([open]) .file-nav-toggle-icon { transform: rotate(-90deg); }
.file-nav-list {
padding: var(--space-1) 0;
list-style: none;
}
.file-nav-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) var(--space-3);
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 {
flex-shrink: 0;
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
font-size: 9px;
font-weight: 700;
font-family: var(--font-mono);
}
.file-status-added { background: var(--color-success-bg); color: var(--color-success); }
.file-status-deleted { background: var(--color-danger-bg); color: var(--color-danger); }
.file-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 { background: var(--color-merged-bg); color: var(--color-merged); }
.file-nav-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-mono);
}
.file-nav-stat {
flex-shrink: 0;
display: flex;
gap: var(--space-1);
font-size: var(--text-xs);
font-family: var(--font-mono);
}
.nav-add { color: var(--color-success); }
.nav-del { color: var(--color-danger); }
.file-nav-dir > details > .file-nav-list { padding-left: var(--space-4); }
.file-nav-dir-toggle {
display: flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
font-size: var(--text-xs);
font-family: var(--font-mono);
color: var(--color-text-muted);
cursor: pointer;
list-style: none;
user-select: none;
}
.file-nav-dir-toggle::-webkit-details-marker { display: none; }
.file-nav-dir > details:not([open]) .file-nav-toggle-icon { transform: rotate(-90deg); }
/* --- Diff file cards --- */
.diff-file {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
}
.diff-file-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
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;
}
.diff-file-header-right {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
flex-wrap: wrap;
}
.diff-file-toggle-icon {
font-size: 0.7em;
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;
}
.diff-rename-arrow {
font-size: var(--text-xs);
color: var(--color-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.diff-stat-add { color: var(--color-success); font-size: var(--text-sm); font-family: var(--font-mono); font-weight: 600; }
.diff-stat-del { color: var(--color-danger); font-size: var(--text-sm); font-family: var(--font-mono); font-weight: 600; }
.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;
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-file-body { }
.diff-binary-notice {
padding: var(--space-4) var(--space-6);
font-size: var(--text-sm);
color: var(--color-text-muted);
font-style: italic;
}
.diff-hunk { border-top: 1px solid var(--color-border-muted); overflow-x: auto; }
.diff-hunk:first-child { border-top: none; }
.diff-hunk-header {
padding: var(--space-1) var(--space-4);
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));
color: var(--color-merged);
border-bottom: 1px solid var(--color-border-muted);
white-space: pre;
}
/* --- Diff table --- */
.diff-table {
min-width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: var(--text-xs);
line-height: 1.5;
}
.diff-ln {
width: 44px;
min-width: 44px;
text-align: right;
padding: 0 var(--space-2);
color: var(--color-text-muted);
border-right: 1px solid var(--color-border-muted);
user-select: none;
vertical-align: top;
white-space: nowrap;
}
.diff-sign {
width: 18px;
min-width: 18px;
text-align: center;
padding: 0 var(--space-1);
user-select: none;
vertical-align: top;
}
.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); }
.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); }
.diff-row-del .diff-ln,
.diff-row-del .diff-sign { background: color-mix(in srgb, var(--color-danger-bg) 90%, transparent); }
.diff-row-add .diff-sign { color: var(--color-success); font-weight: 700; }
.diff-row-del .diff-sign { color: var(--color-danger); font-weight: 700; }
/* Shiki dark-mode support in diff code cells */
@media (prefers-color-scheme: dark) {
.diff-code span[style] { color: var(--shiki-dark) !important; }
}
/* Shiki dark-mode support in file blob view */
@media (prefers-color-scheme: dark) {
.shiki-wrapper .shiki { background-color: var(--shiki-dark-bg) !important; color: var(--shiki-dark) !important; }
.shiki-wrapper .shiki span { color: var(--shiki-dark) !important; }
}
/* --- Issue / Patch list --- */
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
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);
}
.list-header-tabs { display: flex; gap: var(--space-2); }
.list-tab {
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-md);
font-size: var(--text-sm);
text-decoration: none;
color: var(--color-text-muted);
}
.list-tab:hover { background: var(--color-bg-inset); color: var(--color-text); }
.list-tab.active {
background: var(--color-bg-inset);
color: var(--color-text);
font-weight: 600;
}
.issue-list { border: 1px solid var(--color-border); border-top: none; border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
.issue-item {
padding: var(--space-4) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
}
.issue-item:last-child { border-bottom: none; }
.issue-main {
display: flex;
align-items: center;
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;
}
.issue-title:hover { color: var(--color-link); }
.issue-meta {
font-size: var(--text-xs);
color: var(--color-text-muted);
display: flex;
gap: var(--space-3);
}
.issue-status-dot, .patch-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.issue-status-dot.open, .patch-status-dot.open { background: var(--color-success); }
.issue-status-dot.closed, .patch-status-dot.closed { background: var(--color-danger); }
.patch-status-dot.merged { background: var(--color-merged); }
/* --- Issue detail / Timeline --- */
.issue-detail-header {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-6);
flex-wrap: wrap;
}
.issue-number {
font-size: var(--text-2xl);
font-weight: 600;
color: var(--color-text-muted);
white-space: nowrap;
}
.issue-detail-title {
font-size: var(--text-2xl);
font-weight: 600;
flex: 1;
}
.issue-detail-meta-actions { margin-left: auto; display: flex; gap: var(--space-2); }
.timeline-item {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
}
.timeline-author {
background: var(--color-bg-subtle);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-3);
border-bottom: 1px solid var(--color-border);
}
.timeline-author time { color: var(--color-text-muted); font-size: var(--text-xs); }
.timeline-author-right { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); }
.edited-indicator { color: var(--color-text-muted); 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);
}
.timeline-body { padding: var(--space-4); }
.timeline-item-new { padding: var(--space-4); }
.timeline-item-new .section-title { font-size: var(--text-base); border: none; padding-bottom: 0; }
/* Edit <details> (summary only) in .timeline-author; form is a sibling shown via :has() */
.timeline-author-right .inline-edit-details { border: none; }
.timeline-author-right .inline-edit-details > summary { padding: 0; list-style: none; }
.timeline-author-right .inline-edit-details > summary::-webkit-details-marker { display: none; }
.inline-edit-details .when-open { display: none; }
.inline-edit-details[open] .when-closed { display: none; }
.inline-edit-details[open] .when-open { display: inline; }
.inline-edit-form-area { display: none; border-top: 1px solid var(--color-border-muted); }
.timeline-item:has(.inline-edit-details[open]) .inline-edit-form-area { display: block; }
.inline-edit-form { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
/* --- Reactions --- */
.reaction-bar {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border-top: 1px solid var(--color-border-muted);
}
.reaction-form { display: inline; }
.reaction-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-full);
background: var(--color-bg-subtle);
color: var(--color-text);
font-size: var(--text-sm);
cursor: pointer;
transition: background 0.1s;
}
.reaction-btn:hover { background: var(--color-bg-inset); }
.reaction-btn.reacted {
background: var(--color-accent-bg);
border-color: var(--color-accent);
}
.reaction-picker {
position: relative;
}
.reaction-add-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px dashed var(--color-border);
border-radius: var(--radius-full);
background: transparent;
cursor: pointer;
font-size: var(--text-base);
color: var(--color-text-muted);
list-style: none;
}
.reaction-add-btn::-webkit-details-marker { display: none; }
.reaction-picker-dropdown {
position: absolute;
bottom: calc(100% + var(--space-2));
left: 0;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-2);
display: flex;
gap: var(--space-1);
z-index: 10;
}
.reaction-picker-btn {
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
padding: var(--space-1);
border-radius: var(--radius-sm);
}
.reaction-picker-btn:hover { 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;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-sm);
flex-direction: column;
}
.apply-result > div { display: flex; gap: var(--space-2); align-items: center; }
.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);
border-radius: var(--radius-sm);
width: 100%;
}
.patch-actions {
display: flex;
gap: var(--space-3);
margin: var(--space-4) 0;
flex-wrap: wrap;
}
.patch-card-title-row {
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);
}
/* --- 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-section {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.readme-header {
background: var(--color-bg-subtle);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
font-weight: 500;
border-bottom: 1px solid var(--color-border);
}
.readme-section .markdown-body {
padding: var(--space-4);
}
/* --- Code setup block --- */
.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);
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;
}
/* --- Markdown body --- */
.markdown-body {
font-size: var(--text-sm);
line-height: 1.6;
color: var(--color-text);
}
.markdown-body h1, .markdown-body h2, .markdown-body h3,
.markdown-body h4, .markdown-body h5, .markdown-body h6 {
margin-top: var(--space-6);
margin-bottom: var(--space-3);
}
.markdown-body p { margin-bottom: var(--space-4); }
.markdown-body a { color: var(--color-link); }
.markdown-body code {
background: var(--color-bg-inset);
padding: 0.1em 0.3em;
border-radius: var(--radius-sm);
font-size: 0.9em;
}
.markdown-body pre {
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
overflow: auto;
margin-bottom: var(--space-4);
}
.markdown-body pre code { background: none; padding: 0; }
.markdown-body ul, .markdown-body ol {
padding-left: var(--space-6);
margin-bottom: var(--space-4);
}
.markdown-body ul { list-style: disc; }
.markdown-body ol { list-style: decimal; }
.markdown-body li { margin-bottom: var(--space-1); }
.markdown-body blockquote {
border-left: 3px solid var(--color-border);
padding-left: var(--space-4);
color: var(--color-text-muted);
margin: var(--space-4) 0;
}
.markdown-body table {
width: 100%;
border-collapse: collapse;
margin-bottom: var(--space-4);
}
.markdown-body th, .markdown-body td {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
}
.markdown-body th { background: var(--color-bg-subtle); font-weight: 600; }
.markdown-body hr {
border: none;
border-top: 1px solid var(--color-border);
margin: var(--space-6) 0;
}
.markdown-body img { max-width: 100%; border-radius: var(--radius-md); }
.markdown-body details { margin-bottom: var(--space-4); }
.markdown-body summary { cursor: pointer; font-weight: 500; }
/* --- Search form --- */
.search-form { margin-bottom: var(--space-5); }
.search-input-wrap {
display: flex;
gap: var(--space-2);
max-width: 480px;
}
.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);
font-size: var(--text-sm);
}
.search-input:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
border-color: var(--color-accent);
}
/* --- Issue close bar --- */
.issue-close-bar {
display: flex;
justify-content: flex-end;
padding: var(--space-3) 0;
border-top: 1px solid var(--color-border-muted);
margin-top: var(--space-2);
}
/* --- Auth divider --- */
.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);
}
/* --- Passkey UI in settings --- */
.passkey-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); }
/* --- Danger zone --- */
.danger-zone { margin-top: var(--space-8); }
.danger-title { color: var(--color-danger); border-bottom-color: var(--color-danger); }
.danger-card {
border-color: var(--color-danger);
background: var(--color-danger-bg);
}
.danger-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
flex-wrap: wrap;
}
.danger-item p { margin: var(--space-1) 0 0; }
.btn-danger {
background: var(--color-danger);
border-color: transparent;
color: #fff;
}
.btn-danger:hover { filter: brightness(0.9); }
/* --- Release list items --- */
.release-item-header {
display: flex;
gap: var(--space-3);
align-items: flex-start;
}
.release-item-main { flex: 1; min-width: 0; }
.release-item-title {
font-size: var(--text-lg);
font-weight: 700;
text-decoration: none;
color: var(--color-text);
display: block;
margin-bottom: var(--space-1);
}
.release-item-title:hover { color: var(--color-link); }
.release-item-meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.release-item-date {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-2);
font-size: var(--text-xs);
color: var(--color-text-muted);
white-space: nowrap;
flex-shrink: 0;
}
/* --- Release notes preview in list --- */
.release-notes-section { margin-top: var(--space-3); }
.release-notes-label {
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--space-2);
}
.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;
position: relative;
white-space: pre-wrap;
font-size: var(--text-sm);
color: var(--color-text);
line-height: 1.6;
word-break: break-word;
margin-bottom: var(--space-1);
}
.notes-preview::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2.5em;
background: linear-gradient(transparent, var(--color-bg));
pointer-events: none;
}
.notes-expand[open] .notes-preview { display: none; }
.notes-expand[open] { display: flex; flex-direction: column; }
.notes-expand[open] > summary { order: 1; }
.notes-expand[open] > .notes-full { order: 0; }
.notes-toggle-label {
display: inline-block;
font-size: var(--text-xs);
color: var(--color-link);
margin-top: var(--space-1);
}
.notes-toggle-label:hover { text-decoration: underline; }
.notes-toggle-label::before { content: "Show more ↓"; }
.notes-expand[open] .notes-toggle-label::before { content: "Show less ↑"; }
.notes-full { padding-top: var(--space-3); padding-bottom: var(--space-1); }
/* --- Release detail --- */
.release-detail { padding: var(--space-6) 0; }
.release-header { margin-bottom: var(--space-6); }
.release-notes { margin-bottom: var(--space-6); }
.release-assets { margin-bottom: var(--space-6); }
.release-assets-heading {
font-size: var(--text-base);
font-weight: 600;
margin-bottom: var(--space-3);
}
.asset-list {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.asset-item {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
font-size: var(--text-sm);
}
.asset-item:last-child { border-bottom: none; }
.asset-name {
flex: 1;
color: var(--color-link);
text-decoration: none;
font-family: var(--font-mono);
font-size: var(--text-xs);
}
.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;
}
.release-back { margin-top: var(--space-4); display: flex; gap: var(--space-3); align-items: center; justify-content: space-between; }
}
/* ============================================================
@layer utilities
============================================================ */
@layer utilities {
.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; }
}
/* ============================================================
Pagination
============================================================ */
.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; }
.pagination {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
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); }
.ssh-key-info { flex: 1; display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.ssh-key-name { font-weight: 500; }
.ssh-key-fingerprint { font-family: var(--font-mono); font-size: var(--text-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.form-textarea { resize: vertical; min-height: 4.5rem; font-family: var(--font-mono); font-size: var(--text-sm); }
/* Clone popup */
.clone-popup-details { position: relative; display: inline-block; }
.clone-popup-details > summary { list-style: none; }
.clone-popup-details > summary::-webkit-details-marker { display: none; }
.clone-popup { position: absolute; right: 1rem; 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; z-index: 100; box-shadow: 0 4px 20px rgba(0,0,0,0.15); display: flex; flex-direction: column; gap: var(--space-2); }
.clone-url-row { display: flex; align-items: center; gap: var(--space-2); }
.clone-url-label { font-size: var(--text-xs); font-weight: 600; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; min-width: 2.75rem; }
.clone-url-input { flex: 1; font-family: var(--font-mono); font-size: var(--text-sm); background: var(--color-bg-subtle); border: 1px solid var(--color-border-muted); border-radius: var(--radius-md); padding: var(--space-1) var(--space-2); color: var(--color-text); cursor: text; min-width: 0; }
.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); }
▾Ascripts/build-css.ts
@@ -0,0 +1,30 @@
import { watch } from "fs";
import { transform } from "lightningcss";
const INPUT = "src/styles/main.css";
const OUTPUT = "public/assets/main.css";
async function build() {
const source = Bun.file(INPUT);
const { code } = transform({
filename: INPUT,
code: await source.bytes(),
minify: true,
sourceMap: false,
});
await Bun.write(OUTPUT, code);
console.log(`[css] built ${code.length} bytes`);
}
await build();
if (process.argv.includes("--watch")) {
watch(INPUT, async () => {
try {
await build();
} catch (e) {
console.error("[css]", e);
}
});
console.log(`[css] watching ${INPUT}`);
}
▾Ascripts/dev.ts
@@ -0,0 +1,10 @@
const server = Bun.spawn(["bun", "--watch", "src/index.tsx"], { stdio: ["inherit", "inherit", "inherit"] });
const css = Bun.spawn(["bun", "scripts/build-css.ts", "--watch"], { stdio: ["inherit", "inherit", "inherit"] });
process.on("SIGINT", () => {
server.kill();
css.kill();
process.exit(0);
});
await Promise.all([server.exited, css.exited]);
▾Msrc/config.ts
@@ -15,6 +15,11 @@ export const PORT = parseInt(process.env.PORT ?? "", 10) || 3000;
export const SSH_PORT = parseInt(process.env.SSH_PORT ?? "", 10) || 2222;
export const REGISTRATION_DISABLED = !!process.env.REGISTRATION_DISABLED;
export const BASE_URL = process.env.BASE_URL ?? `http://localhost:${PORT}`;
export const GIT_AUTHOR_NAME =
process.env.GIT_AUTHOR_NAME ?? OWNER_DISPLAY_NAME;
export const GIT_AUTHOR_EMAIL =
process.env.GIT_AUTHOR_EMAIL ??
`${OWNER_DISPLAY_NAME}@${new URL(BASE_URL).hostname}`;
export const DATA_DIR = path.resolve(process.env.DATA_DIR ?? "./data");
export const HIGHLIGHT_WORKERS =
parseInt(process.env.HIGHLIGHT_WORKERS ?? "", 10) || 4;
▾Msrc/routes/avatars.ts
@@ -29,7 +29,19 @@ export const avatarRoutes = new Elysia()
if (!new URL(request.url).searchParams.has("v"))
return new Response("Not found", { status: 404 });
return new Response(Bun.file(avatarJxlPath(userId)), {
let file = Bun.file(avatarJxlPath(userId));
if (!(await file.exists())) {
const user = await db
.selectFrom("users")
.select("username")
.where("id", "=", userId)
.executeTakeFirst();
if (!user) return new Response("Not found", { status: 404 });
await createDefaultAvatar(userId, user.username);
file = Bun.file(avatarJxlPath(userId));
}
return new Response(file, {
headers: {
"Content-Type": "image/jxl",
"Cache-Control": CACHE,
▾Msrc/routes/issues.tsx
@@ -28,7 +28,9 @@ export const issueRoutes = new Elysia()
const status =
query.status === "closed"
? ("closed" as const)
: ("open" as const);
: query.status === "completed"
? ("completed" as const)
: ("open" as const);
const page = Math.max(1, query.page ?? 1);
const allCounts = await db
@@ -62,6 +64,7 @@ export const issueRoutes = new Elysia()
"issues.updated_at",
"issues.edited_at",
"users.username as author_username",
"users.avatar_version as author_avatar_version",
])
.where("issues.repo_id", "=", repo.id)
.where("issues.status", "=", status)
@@ -82,6 +85,7 @@ export const issueRoutes = new Elysia()
issues={
issues as ((typeof issues)[0] & {
author_username: string;
author_avatar_version: number | null;
})[]
}
status={status}
@@ -394,6 +398,34 @@ export const issueRoutes = new Elysia()
},
)
.post("/:repo/issues/:number/complete", async ({ params, cookie }) => {
const user = await resolveSession(cookie.session.value);
const deny = requireAdmin(user);
if (deny) return deny;
const repo = await getRepo(params.repo, true);
if (!repo) return new Response("Not found", { status: 404 });
const issueNum = parseInt(params.number, 10);
const issue = await db
.selectFrom("issues")
.select("id")
.where("repo_id", "=", repo.id)
.where("number", "=", issueNum)
.executeTakeFirst();
if (!issue) return new Response("Not found", { status: 404 });
await db
.updateTable("issues")
.set({ status: "completed", updated_at: new Date().toISOString() })
.where("id", "=", issue.id)
.execute();
return new Response(null, {
status: 302,
headers: { Location: `/${repo.name}/issues/${issueNum}` },
});
})
.post("/:repo/issues/:number/close", async ({ params, cookie }) => {
const user = await resolveSession(cookie.session.value);
const deny = requireAdmin(user);
▾Msrc/routes/patches.tsx
@@ -94,6 +94,7 @@ export const patchRoutes = new Elysia()
"patches.updated_at",
"patches.edited_at",
"users.username as author_username",
"users.avatar_version as author_avatar_version",
])
.where("patches.repo_id", "=", repo.id)
.where("patches.status", "=", status)
@@ -114,6 +115,7 @@ export const patchRoutes = new Elysia()
patches={
patches as ((typeof patches)[0] & {
author_username: string;
author_avatar_version: number | null;
})[]
}
status={status}
▾Msrc/routes/repos.tsx
@@ -38,8 +38,8 @@ async function mimeForContent(content: Buffer): Promise<string> {
const result = await fileTypeFromBuffer(content);
if (result) return result.mime;
return hasBinaryContent(content.subarray(0, 8000))
? "text/plain; charset=utf-8"
: "application/octet-stream";
? "application/octet-stream"
: "text/plain; charset=utf-8";
}
async function readReadme(
▾Msrc/services/git.ts
@@ -1,6 +1,7 @@
import path from "node:path";
import { $ as _$ } from "bun";
import { GIT_AUTHOR_EMAIL, GIT_AUTHOR_NAME } from "../config.ts";
import { REPOS_DIR } from "../constants.ts";
const $ = _$.env({ ...process.env, LC_ALL: "C", LANG: "C" });
@@ -367,7 +368,17 @@ export const git = {
: title;
const msg = extractPatchSubject(patchContent) || fallback;
const commit = (
await $`git -C ${p} commit-tree ${tree} -p ${parent} -m ${msg}`.text()
await $`git -C ${p} commit-tree ${tree} -p ${parent} -m ${msg}`
.env({
...process.env,
LC_ALL: "C",
LANG: "C",
GIT_AUTHOR_NAME,
GIT_AUTHOR_EMAIL,
GIT_COMMITTER_NAME: GIT_AUTHOR_NAME,
GIT_COMMITTER_EMAIL: GIT_AUTHOR_EMAIL,
})
.text()
).trim();
const ref = (
await $`git -C ${p} symbolic-ref HEAD`.text()
▾Msrc/services/highlight.ts
@@ -96,6 +96,58 @@ export type FileView =
const fileCache = new Map<string, FileView>();
function shikiHtmlToTable(html: string): string {
const styleMatch = html.match(/<pre[^>]*style="([^"]*)"/);
const rawStyle = styleMatch?.[1] ?? "";
// Convert Shiki's direct background-color/color to CSS custom properties so
// the CSS (not the inline style) controls the background in dark mode.
const lightBg =
rawStyle.match(/(?:^|;)background-color:([^;]+)/)?.[1] ?? "#fff";
const lightColor = rawStyle.match(/(?:^|;)color:([^;]+)/)?.[1] ?? "";
const darkBg = rawStyle.match(/--shiki-dark-bg:([^;]+)/)?.[1] ?? "";
const darkColor = rawStyle.match(/--shiki-dark:([^;]+)/)?.[1] ?? "";
const varParts = [`--shiki-light-bg:${lightBg}`];
if (lightColor) varParts.push(`--shiki-light:${lightColor}`);
if (darkBg) varParts.push(`--shiki-dark-bg:${darkBg}`);
if (darkColor) varParts.push(`--shiki-dark:${darkColor}`);
const tableStyle = varParts.join(";");
const codeMatch = html.match(/<code>([\s\S]*?)<\/code>/);
const codeContent = codeMatch?.[1] ?? "";
const rawLines = codeContent.split("\n");
// Shiki adds a trailing newline, so drop the last empty entry
const lines =
rawLines.length > 0 && rawLines[rawLines.length - 1] === ""
? rawLines.slice(0, -1)
: rawLines;
const rows = lines
.map((line, i) => {
const n = i + 1;
return `<tr id="L${n}"><td class="blob-ln"><a href="#L${n}">${n}</a></td><td class="blob-code">${line}</td></tr>`;
})
.join("");
return `<table class="blob-table" style="${tableStyle}"><tbody>${rows}</tbody></table>`;
}
function plainTextToTable(text: string): string {
const rawLines = text.split("\n");
const lines =
rawLines.length > 0 && rawLines[rawLines.length - 1] === ""
? rawLines.slice(0, -1)
: rawLines;
const rows = lines
.map((line, i) => {
const n = i + 1;
const escaped = line
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return `<tr id="L${n}"><td class="blob-ln"><a href="#L${n}">${n}</a></td><td class="blob-code">${escaped}</td></tr>`;
})
.join("");
return `<table class="blob-table"><tbody>${rows}</tbody></table>`;
}
export async function serveFile(
content: Buffer,
filename: string,
@@ -112,26 +164,25 @@ export async function serveFile(
return { type: "download", size: content.length };
}
const text = content.toString("utf-8");
const lines = text.split("\n").length;
const lineCount = text.split("\n").length;
let view: FileView;
try {
const h = getHighlighter();
const lang = detectLang(filename);
const html = h.codeToHtml(text, {
const shikiHtml = h.codeToHtml(text, {
lang,
themes: { light: "github-light", dark: "github-dark" },
});
view = { type: "inline", html, lines };
view = {
type: "inline",
html: shikiHtmlToTable(shikiHtml),
lines: lineCount,
};
} catch {
// fallback: plain pre
const escaped = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
view = {
type: "inline",
html: `<pre class="code-plain"><code>${escaped}</code></pre>`,
lines,
html: plainTextToTable(text),
lines: lineCount,
};
}
if (cacheKey) {
▾Asrc/styles/main.css
@@ -0,0 +1,2486 @@
/* View Transitions */
@view-transition {
navigation: auto;
}
.site-nav {
view-transition-name: site-nav;
}
.repo-header {
view-transition-name: repo-header;
}
.breadcrumb {
view-transition-name: breadcrumb;
}
/* ============================================================
@layer reset
============================================================ */
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
min-height: 100dvh;
min-width: 500px;
}
img,
video {
max-width: 100%;
height: auto;
}
button,
input,
textarea,
select {
font: inherit;
}
a {
color: inherit;
}
ul,
ol {
list-style: none;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
line-height: 1.2;
}
pre,
code {
font-family: var(--font-mono);
}
}
/* ============================================================
@layer tokens
============================================================ */
@layer tokens {
:root {
/* Colors — light */
--color-bg: #ffffff;
--color-bg-subtle: #f6f8fa;
--color-bg-inset: #eef0f2;
--color-border: #d0d7de;
--color-border-muted: #e4e7eb;
--color-text: #1f2328;
--color-text-muted: #656d76;
--color-link: #0969da;
--color-link-hover: #0550ae;
--color-accent: #fd7e14;
--color-accent-bg: #fff3cd;
--color-success: #1a7f37;
--color-success-bg: #dafbe1;
--color-danger: #cf222e;
--color-danger-bg: #ffebe9;
--color-warning: #9a6700;
--color-warning-bg: #fff8c5;
--color-merged: #8250df;
--color-merged-bg: #fbefff;
/* Typography */
--font-sans:
system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono:
ui-monospace, "Cascadia Code", "Source Code Pro", monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--space-16: 4rem;
/* Border radius */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-full: 9999px;
/* Shadows */
--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);
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0d1117;
--color-bg-subtle: #161b22;
--color-bg-inset: #1c2128;
--color-border: #30363d;
--color-border-muted: #21262d;
--color-text: #e6edf3;
--color-text-muted: #7d8590;
--color-link: #58a6ff;
--color-link-hover: #79c0ff;
--color-accent: #f0883e;
--color-accent-bg: #3d2f00;
--color-success: #3fb950;
--color-success-bg: #0d2b19;
--color-danger: #f85149;
--color-danger-bg: #2d1419;
--color-warning: #d29922;
--color-warning-bg: #2c2000;
--color-merged: #bc8cff;
--color-merged-bg: #1e0b3e;
}
}
/* Explicit theme overrides — applied via data-theme attribute set by cookie */
:root[data-theme="dark"] {
--color-bg: #0d1117;
--color-bg-subtle: #161b22;
--color-bg-inset: #1c2128;
--color-border: #30363d;
--color-border-muted: #21262d;
--color-text: #e6edf3;
--color-text-muted: #7d8590;
--color-link: #58a6ff;
--color-link-hover: #79c0ff;
--color-accent: #f0883e;
--color-accent-bg: #3d2f00;
--color-success: #3fb950;
--color-success-bg: #0d2b19;
--color-danger: #f85149;
--color-danger-bg: #2d1419;
--color-warning: #d29922;
--color-warning-bg: #2c2000;
--color-merged: #bc8cff;
--color-merged-bg: #1e0b3e;
}
:root[data-theme="light"] {
--color-bg: #ffffff;
--color-bg-subtle: #f6f8fa;
--color-bg-inset: #eef0f2;
--color-border: #d0d7de;
--color-border-muted: #e4e7eb;
--color-text: #1f2328;
--color-text-muted: #656d76;
--color-link: #0969da;
--color-link-hover: #0550ae;
--color-accent: #fd7e14;
--color-accent-bg: #fff3cd;
--color-success: #1a7f37;
--color-success-bg: #dafbe1;
--color-danger: #cf222e;
--color-danger-bg: #ffebe9;
--color-warning: #9a6700;
--color-warning-bg: #fff8c5;
--color-merged: #8250df;
--color-merged-bg: #fbefff;
}
}
/* ============================================================
@layer layout
============================================================ */
@layer layout {
body {
font-family: var(--font-sans);
font-size: var(--text-base);
color: var(--color-text);
background: var(--color-bg);
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100dvh;
}
.site-header {
background: var(--color-bg-subtle);
border-bottom: 1px solid var(--color-border);
position: sticky;
top: 0;
z-index: 100;
}
.site-nav {
max-width: 1200px;
margin: 0 auto;
padding: var(--space-3) var(--space-4);
display: flex;
align-items: center;
gap: var(--space-4);
}
.site-logo {
display: flex;
align-items: center;
gap: var(--space-2);
text-decoration: none;
font-weight: 700;
font-size: var(--text-lg);
color: var(--color-text);
}
.logo-icon {
width: 1.6em;
height: 1.6em;
display: block;
flex-shrink: 0;
}
.nav-links {
margin-left: auto;
display: flex;
align-items: center;
gap: var(--space-3);
}
.nav-user {
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.site-main {
padding: var(--space-8) var(--space-4);
min-width: 0;
}
.site-footer {
text-align: center;
padding: var(--space-6) var(--space-4);
font-size: var(--text-sm);
color: var(--color-text-muted);
border-top: 1px solid var(--color-border);
}
.container {
max-width: 1000px;
margin: 0 auto;
overflow-x: clip;
}
.container-narrow {
max-width: 680px;
}
.inline-form {
display: inline;
}
/* --- Two-step delete confirmation --- */
.confirm-details {
position: relative;
display: inline-block;
}
.confirm-details > summary {
list-style: none;
}
.confirm-details > summary::-webkit-details-marker {
display: none;
}
.confirm-popup {
position: absolute;
right: 0;
top: calc(100% + var(--space-1));
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
display: flex;
align-items: center;
gap: var(--space-3);
white-space: nowrap;
z-index: 100;
font-size: var(--text-sm);
color: var(--color-text-muted);
}
}
/* ============================================================
@layer components
============================================================ */
@layer components {
/* --- Buttons --- */
.btn {
display: inline-flex;
align-items: 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;
text-decoration: none;
cursor: pointer;
background: var(--color-bg-subtle);
color: var(--color-text);
transition:
background 0.1s,
border-color 0.1s;
white-space: nowrap;
}
.btn:hover {
background: var(--color-bg-inset);
border-color: var(--color-border);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: var(--color-accent);
border-color: transparent;
color: #fff;
}
.btn-primary:hover {
background: color-mix(in srgb, var(--color-accent) 82%, #000);
}
.btn-secondary {
background: var(--color-bg-subtle);
border-color: var(--color-border);
}
.btn-ghost {
background: transparent;
border-color: transparent;
}
.btn-ghost:hover {
background: var(--color-bg-inset);
}
/* Keep ghost transparent even when combined with size variants */
.btn-ghost.btn-xs,
.btn-ghost.btn-sm {
background: transparent;
border-color: transparent;
}
.btn-ghost.btn-xs:hover,
.btn-ghost.btn-sm:hover {
background: var(--color-bg-inset);
}
.btn-sm {
padding: var(--space-1) var(--space-3);
font-size: var(--text-xs);
}
.btn-block {
width: 100%;
justify-content: center;
}
/* --- Forms --- */
.form-group {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.form-group label {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text);
}
.form-group input:not([type="file"]):not([type="checkbox"]),
.form-group textarea,
.form-group select {
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);
}
.form-group input:not([type="file"]):not([type="checkbox"]):focus,
.form-group textarea:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
border-color: var(--color-accent);
}
.form-group input[type="file"] {
font-size: var(--text-sm);
color: var(--color-text);
padding: 0;
border: none;
background: none;
}
.form-group textarea {
resize: vertical;
min-height: 120px;
}
.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);
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-error {
background: var(--color-danger-bg);
color: var(--color-danger);
border: 1px solid var(--color-danger);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
margin-bottom: var(--space-4);
}
.form-actions {
display: flex;
gap: var(--space-3);
align-items: center;
flex-wrap: wrap;
}
.form-card {
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
margin-top: var(--space-4);
}
.checkbox-label {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
}
/* --- Page header --- */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-6);
gap: var(--space-4);
}
.page-title {
font-size: var(--text-2xl);
color: var(--color-text);
margin-bottom: var(--space-4);
}
.page-title a {
text-decoration: none;
color: var(--color-link);
}
.page-title a:hover {
text-decoration: underline;
}
.section-title {
font-size: var(--text-lg);
margin-bottom: var(--space-4);
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
/* --- Auth --- */
.auth-container {
max-width: 400px;
margin: var(--space-12) auto;
}
.auth-container .page-title {
margin-bottom: var(--space-6);
}
.auth-form {
margin-bottom: var(--space-4);
}
.auth-footer {
text-align: center;
font-size: var(--text-sm);
color: var(--color-text-muted);
margin-top: var(--space-4);
}
.passkey-btn {
display: none;
margin-top: var(--space-3);
}
/* --- Repo header --- */
.repo-header {
margin-bottom: var(--space-2);
}
.repo-title-row {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
}
.repo-title-row .page-title {
margin-bottom: 0;
}
.repo-header-title {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
}
.repo-description {
color: var(--color-text-muted);
margin-top: var(--space-2);
}
.repo-header-meta {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-3);
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.icon {
font-style: normal;
}
/* --- Branch selector --- */
.branch-selector {
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.branch-selector-icon {
color: var(--color-text-muted);
font-size: var(--text-sm);
user-select: none;
}
.branch-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;
}
.branch-select:hover {
border-color: var(--color-text-muted);
}
.branch-select:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
}
/* toolbar row above file tree */
.tree-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-3);
}
/* commits-header flex row */
.commits-header {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.commits-header .section-title {
margin-bottom: 0;
}
/* --- Repo nav bar (tabs + private badge) --- */
.repo-nav-bar {
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;
}
.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;
}
.repo-tab:hover {
color: var(--color-text);
}
.repo-tab.active {
color: var(--color-text);
font-weight: 600;
border-bottom-color: var(--color-accent);
}
/* Align active indicator with nav-bar border */
.repo-tab {
margin-bottom: -1px;
}
/* --- Repo list --- */
.repo-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.repo-card {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: var(--space-4) var(--space-5);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-bg);
gap: var(--space-4);
transition: border-color 0.15s;
}
.repo-card:hover {
border-color: var(--color-accent);
}
.repo-card-main {
flex: 1;
min-width: 0;
}
.repo-card-title {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
margin-bottom: var(--space-1);
}
.repo-name {
font-size: var(--text-lg);
font-weight: 600;
color: var(--color-link);
text-decoration: none;
}
.repo-name:hover {
text-decoration: underline;
}
.repo-card-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-1);
font-size: var(--text-xs);
color: var(--color-text-muted);
white-space: nowrap;
}
.repo-date {
color: var(--color-text-muted);
}
/* --- Badges --- */
.badge {
display: inline-flex;
align-items: center;
padding: 1px var(--space-2);
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid;
}
.badge-private {
color: var(--color-warning);
border-color: var(--color-warning);
background: var(--color-warning-bg);
}
.issue-badge,
.patch-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;
text-transform: capitalize;
}
.issue-badge.open,
.patch-badge.open {
background: var(--color-success-bg);
color: var(--color-success);
}
.issue-badge.closed,
.patch-badge.closed {
background: var(--color-danger-bg);
color: var(--color-danger);
}
.issue-badge.completed {
background: var(--color-merged-bg);
color: var(--color-merged);
}
.patch-badge.merged {
background: var(--color-merged-bg);
color: var(--color-merged);
}
/* --- Empty state --- */
.empty-state {
text-align: center;
padding: var(--space-16) var(--space-4);
color: var(--color-text-muted);
}
.empty-state p {
margin-bottom: var(--space-4);
font-size: var(--text-lg);
}
/* --- Breadcrumb --- */
.breadcrumb {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1);
font-size: var(--text-sm);
margin-bottom: var(--space-4);
}
.breadcrumb a {
color: var(--color-link);
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb-sep {
color: var(--color-text-muted);
}
.breadcrumb-current {
color: var(--color-text-muted);
}
/* --- File tree --- */
.file-tree {
width: 100%;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
border-collapse: collapse;
overflow: hidden;
margin-bottom: var(--space-6);
}
.file-tree-row {
border-bottom: 1px solid var(--color-border-muted);
}
.file-tree-row:last-child {
border-bottom: none;
}
.file-tree-row:hover {
background: var(--color-bg-subtle);
}
.file-icon,
.file-name,
.file-size {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
vertical-align: middle;
}
.file-icon {
width: 36px;
padding-right: var(--space-1);
text-align: center;
line-height: 0;
}
.file-name {
width: 100%;
}
.file-name a {
color: var(--color-link);
text-decoration: none;
}
.file-name a:hover {
text-decoration: underline;
}
.file-name a.file-name-dir {
font-weight: 500;
}
.file-size {
text-align: right;
color: var(--color-text-muted);
font-size: var(--text-xs);
white-space: nowrap;
padding-right: var(--space-4);
}
.file-icon-dir {
color: var(--color-link);
}
.file-icon-file {
color: var(--color-text-muted);
}
.tree-icon {
display: inline-block;
vertical-align: text-bottom;
}
.file-tree-row-up .file-name a {
color: var(--color-text-muted);
}
.file-tree-row-up .file-name a:hover {
color: var(--color-link);
}
/* --- File blob --- */
.file-blob-header {
display: flex;
align-items: center;
justify-content: space-between;
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;
}
.file-blob-name {
font-size: var(--text-sm);
font-weight: 500;
}
.file-blob-body {
border: 1px solid var(--color-border);
border-top: none;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
overflow: auto;
}
/* Blob line-number table */
.blob-table {
min-width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: var(--text-sm);
line-height: 1.6;
background-color: var(--shiki-light-bg, #fff);
color: var(--shiki-light, inherit);
}
.blob-ln {
width: 1%;
min-width: 48px;
text-align: right;
padding: 0 var(--space-3);
color: var(--color-text-muted);
border-right: 1px solid var(--color-border-muted);
user-select: none;
white-space: nowrap;
vertical-align: top;
}
.blob-ln a {
color: inherit;
text-decoration: none;
}
.blob-ln a:hover {
color: var(--color-link);
}
.blob-code {
padding: 0 var(--space-4);
white-space: pre;
overflow: visible;
vertical-align: top;
}
.blob-table tr:target {
background: color-mix(in srgb, #ddf4ff 60%, transparent);
}
.blob-table tr:target .blob-ln {
background: color-mix(in srgb, #ddf4ff 80%, transparent);
}
.file-download-notice {
padding: var(--space-8);
text-align: center;
color: var(--color-text-muted);
}
.file-download-notice p {
margin-bottom: var(--space-4);
}
/* --- Commit list --- */
.commit-list {
display: flex;
flex-direction: column;
}
.commit-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: var(--space-3) 0;
border-bottom: 1px solid var(--color-border-muted);
gap: var(--space-4);
flex-wrap: wrap;
}
.commit-item:last-child {
border-bottom: none;
}
.commit-main {
flex: 1;
min-width: 0;
}
.commit-subject {
color: var(--color-text);
text-decoration: none;
font-size: var(--text-sm);
font-weight: 500;
}
.commit-subject:hover {
color: var(--color-link);
}
.commit-meta {
display: flex;
align-items: center;
gap: var(--space-3);
font-size: var(--text-xs);
color: var(--color-text-muted);
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;
}
.mono {
font-family: var(--font-mono);
}
/* --- Commit detail viewer --- */
.commit-page-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-4);
}
.commit-card {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
}
.commit-card-subject {
font-size: var(--text-xl);
font-weight: 600;
padding: var(--space-5) var(--space-6);
border-bottom: 1px solid var(--color-border);
line-height: 1.3;
}
.commit-card-body {
padding: var(--space-4) 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;
}
.commit-card-meta {
padding: var(--space-4) var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-2);
background: var(--color-bg-subtle);
}
.commit-card-meta-row {
display: flex;
align-items: baseline;
gap: 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;
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.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);
}
/* --- Commit layout (sidebar + diffs) --- */
.commit-layout {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-5);
align-items: start;
}
.commit-diffs {
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;
}
}
/* --- 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);
background: var(--color-bg-subtle);
overflow: hidden;
}
.file-nav-toggle {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
list-style: none;
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;
transition: transform 0.15s;
}
.file-nav-details:not([open]) .file-nav-toggle-icon {
transform: rotate(-90deg);
}
.file-nav-list {
padding: var(--space-1) 0;
list-style: none;
}
.file-nav-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) var(--space-3);
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 {
flex-shrink: 0;
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
font-size: 9px;
font-weight: 700;
font-family: var(--font-mono);
}
.file-status-added {
background: var(--color-success-bg);
color: var(--color-success);
}
.file-status-deleted {
background: var(--color-danger-bg);
color: var(--color-danger);
}
.file-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 {
background: var(--color-merged-bg);
color: var(--color-merged);
}
.file-nav-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-mono);
}
.file-nav-stat {
flex-shrink: 0;
display: flex;
gap: var(--space-1);
font-size: var(--text-xs);
font-family: var(--font-mono);
}
.nav-add {
color: var(--color-success);
}
.nav-del {
color: var(--color-danger);
}
.file-nav-dir > details > .file-nav-list {
padding-left: var(--space-4);
}
.file-nav-dir-toggle {
display: flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
font-size: var(--text-xs);
font-family: var(--font-mono);
color: var(--color-text-muted);
cursor: pointer;
list-style: none;
user-select: none;
}
.file-nav-dir-toggle::-webkit-details-marker {
display: none;
}
.file-nav-dir > details:not([open]) .file-nav-toggle-icon {
transform: rotate(-90deg);
}
/* --- Diff file cards --- */
.diff-file {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
}
.diff-file-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
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;
}
.diff-file-header-right {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
flex-wrap: wrap;
}
.diff-file-toggle-icon {
font-size: 0.7em;
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;
}
.diff-rename-arrow {
font-size: var(--text-xs);
color: var(--color-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.diff-stat-add {
color: var(--color-success);
font-size: var(--text-sm);
font-family: var(--font-mono);
font-weight: 600;
}
.diff-stat-del {
color: var(--color-danger);
font-size: var(--text-sm);
font-family: var(--font-mono);
font-weight: 600;
}
.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;
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);
font-size: var(--text-sm);
color: var(--color-text-muted);
font-style: italic;
}
.diff-hunk {
border-top: 1px solid var(--color-border-muted);
overflow-x: auto;
}
.diff-hunk:first-child {
border-top: none;
}
.diff-hunk-header {
padding: var(--space-1) var(--space-4);
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)
);
color: var(--color-merged);
border-bottom: 1px solid var(--color-border-muted);
white-space: pre;
}
/* --- Diff table --- */
.diff-table {
min-width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: var(--text-xs);
line-height: 1.5;
}
.diff-ln {
width: 44px;
min-width: 44px;
text-align: right;
padding: 0 var(--space-2);
color: var(--color-text-muted);
border-right: 1px solid var(--color-border-muted);
user-select: none;
vertical-align: top;
white-space: nowrap;
}
.diff-sign {
width: 18px;
min-width: 18px;
text-align: center;
padding: 0 var(--space-1);
user-select: none;
vertical-align: top;
}
.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
);
}
.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
);
}
.diff-row-del .diff-ln,
.diff-row-del .diff-sign {
background: color-mix(in srgb, var(--color-danger-bg) 90%, transparent);
}
.diff-row-add .diff-sign {
color: var(--color-success);
font-weight: 700;
}
.diff-row-del .diff-sign {
color: var(--color-danger);
font-weight: 700;
}
/* Shiki dark-mode support in diff code cells */
@media (prefers-color-scheme: dark) {
.diff-code span[style] {
/* biome-ignore lint/complexity/noImportantStyles: required to override shiki inline styles */
color: var(--shiki-dark) !important;
}
}
/* Shiki dark-mode support in file blob view */
@media (prefers-color-scheme: dark) {
.blob-table {
background-color: var(--shiki-dark-bg);
color: var(--shiki-dark);
}
.blob-table span[style] {
/* biome-ignore lint/complexity/noImportantStyles: required to override shiki inline styles */
color: var(--shiki-dark) !important;
}
.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);
}
}
:root[data-theme="dark"] .blob-table {
background-color: var(--shiki-dark-bg);
color: var(--shiki-dark);
}
:root[data-theme="dark"] .blob-table span[style] {
/* biome-ignore lint/complexity/noImportantStyles: required to override shiki inline styles */
color: var(--shiki-dark) !important;
}
/* --- Issue / Patch list --- */
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
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);
}
.list-header-tabs {
display: flex;
gap: var(--space-2);
}
.list-tab {
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-md);
font-size: var(--text-sm);
text-decoration: none;
color: var(--color-text-muted);
}
.list-tab:hover {
background: var(--color-bg-inset);
color: var(--color-text);
}
.list-tab.active {
background: var(--color-bg-inset);
color: var(--color-text);
font-weight: 600;
}
.issue-list {
border: 1px solid var(--color-border);
border-top: none;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.issue-item {
padding: var(--space-4) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
}
.issue-item:last-child {
border-bottom: none;
}
.issue-main {
display: flex;
align-items: center;
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;
}
.issue-title:hover {
color: var(--color-link);
}
.issue-meta {
font-size: var(--text-xs);
color: var(--color-text-muted);
display: flex;
gap: var(--space-3);
}
.issue-status-dot,
.patch-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.issue-status-dot.open,
.patch-status-dot.open {
background: var(--color-success);
}
.issue-status-dot.closed,
.patch-status-dot.closed {
background: var(--color-danger);
}
.issue-status-dot.completed {
background: var(--color-merged);
}
.patch-status-dot.merged {
background: var(--color-merged);
}
/* --- Issue detail / Timeline --- */
.issue-detail-header {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-6);
flex-wrap: wrap;
}
.issue-number {
font-size: var(--text-2xl);
font-weight: 600;
color: var(--color-text-muted);
white-space: nowrap;
}
.issue-detail-title {
font-size: var(--text-2xl);
font-weight: 600;
flex: 1;
}
.issue-detail-meta-actions {
margin-left: auto;
display: flex;
gap: var(--space-2);
}
.timeline-item {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
}
.timeline-author {
background: var(--color-bg-subtle);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-3);
border-bottom: 1px solid var(--color-border);
}
.timeline-author time {
color: var(--color-text-muted);
font-size: var(--text-xs);
}
.timeline-author-right {
margin-left: auto;
display: flex;
align-items: center;
gap: var(--space-2);
}
.edited-indicator {
color: var(--color-text-muted);
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);
}
.timeline-body {
padding: var(--space-4);
}
.timeline-item-new {
padding: var(--space-4);
}
.timeline-item-new .section-title {
font-size: var(--text-base);
border: none;
padding-bottom: 0;
}
/* Edit <details> (summary only) in .timeline-author; form is a sibling shown via :has() */
.timeline-author-right .inline-edit-details {
border: none;
}
.timeline-author-right .inline-edit-details > summary {
padding: 0;
list-style: none;
}
.timeline-author-right
.inline-edit-details
> summary::-webkit-details-marker {
display: none;
}
.inline-edit-details .when-open {
display: none;
}
.inline-edit-details[open] .when-closed {
display: none;
}
.inline-edit-details[open] .when-open {
display: inline;
}
.inline-edit-form-area {
display: none;
border-top: 1px solid var(--color-border-muted);
}
.timeline-item:has(.inline-edit-details[open]) .inline-edit-form-area {
display: block;
}
.inline-edit-form {
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
/* --- Reactions --- */
.reaction-bar {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border-top: 1px solid var(--color-border-muted);
}
.reaction-form {
display: inline;
}
.reaction-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-full);
background: var(--color-bg-subtle);
color: var(--color-text);
font-size: var(--text-sm);
cursor: pointer;
transition: background 0.1s;
}
.reaction-btn:hover {
background: var(--color-bg-inset);
}
.reaction-btn.reacted {
background: var(--color-accent-bg);
border-color: var(--color-accent);
}
.reaction-picker {
position: relative;
}
.reaction-add-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px dashed var(--color-border);
border-radius: var(--radius-full);
background: transparent;
cursor: pointer;
font-size: var(--text-base);
color: var(--color-text-muted);
list-style: none;
}
.reaction-add-btn::-webkit-details-marker {
display: none;
}
.reaction-picker-dropdown {
position: absolute;
bottom: calc(100% + var(--space-2));
left: 0;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-2);
display: flex;
gap: var(--space-1);
z-index: 10;
}
.reaction-picker-btn {
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
padding: var(--space-1);
border-radius: var(--radius-sm);
}
.reaction-picker-btn:hover {
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;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-sm);
flex-direction: column;
}
.apply-result > div {
display: flex;
gap: var(--space-2);
align-items: center;
}
.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);
border-radius: var(--radius-sm);
width: 100%;
}
.patch-actions {
display: flex;
gap: var(--space-3);
margin: var(--space-4) 0;
flex-wrap: wrap;
}
.patch-card-title-row {
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);
}
/* --- 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-section {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.readme-header {
background: var(--color-bg-subtle);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
font-weight: 500;
border-bottom: 1px solid var(--color-border);
}
/* --- Code setup block --- */
.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);
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;
}
/* --- Markdown body --- */
.markdown-body {
font-size: var(--text-sm);
line-height: 1.6;
color: var(--color-text);
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: var(--space-6);
margin-bottom: var(--space-3);
}
.markdown-body p {
margin-bottom: var(--space-4);
}
.markdown-body a {
color: var(--color-link);
}
.markdown-body code {
background: var(--color-bg-inset);
padding: 0.1em 0.3em;
border-radius: var(--radius-sm);
font-size: 0.9em;
}
.markdown-body pre {
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
overflow: auto;
margin-bottom: var(--space-4);
}
.markdown-body pre code {
background: none;
padding: 0;
}
.markdown-body ul,
.markdown-body ol {
padding-left: var(--space-6);
margin-bottom: var(--space-4);
}
.markdown-body ul {
list-style: disc;
}
.markdown-body ol {
list-style: decimal;
}
.markdown-body li {
margin-bottom: var(--space-1);
}
.markdown-body blockquote {
border-left: 3px solid var(--color-border);
padding-left: var(--space-4);
color: var(--color-text-muted);
margin: var(--space-4) 0;
}
.markdown-body table {
width: 100%;
border-collapse: collapse;
margin-bottom: var(--space-4);
}
.markdown-body th,
.markdown-body td {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
}
.markdown-body th {
background: var(--color-bg-subtle);
font-weight: 600;
}
.markdown-body hr {
border: none;
border-top: 1px solid var(--color-border);
margin: var(--space-6) 0;
}
.markdown-body img {
max-width: 100%;
border-radius: var(--radius-md);
}
.markdown-body details {
margin-bottom: var(--space-4);
}
.markdown-body summary {
cursor: pointer;
font-weight: 500;
}
.readme-section .markdown-body {
padding: var(--space-4);
}
/* --- Search form --- */
.search-form {
margin-bottom: var(--space-5);
}
.search-input-wrap {
display: flex;
gap: var(--space-2);
max-width: 480px;
}
.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);
font-size: var(--text-sm);
}
.search-input:focus {
outline: 2px solid var(--color-accent);
outline-offset: -1px;
border-color: var(--color-accent);
}
/* --- Issue close bar --- */
.issue-close-bar {
display: flex;
justify-content: flex-end;
padding: var(--space-3) 0;
border-top: 1px solid var(--color-border-muted);
margin-top: var(--space-2);
}
/* --- Auth divider --- */
.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);
}
/* --- Passkey UI in settings --- */
.passkey-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);
}
/* --- Danger zone --- */
.danger-zone {
margin-top: var(--space-8);
}
.danger-title {
color: var(--color-danger);
border-bottom-color: var(--color-danger);
}
.danger-card {
border-color: var(--color-danger);
background: var(--color-danger-bg);
}
.danger-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
flex-wrap: wrap;
}
.danger-item p {
margin: var(--space-1) 0 0;
}
.btn-danger {
background: var(--color-danger);
border-color: transparent;
color: #fff;
}
.btn-danger:hover {
filter: brightness(0.9);
}
/* --- Release list items --- */
.release-item-header {
display: flex;
gap: var(--space-3);
align-items: flex-start;
}
.release-item-main {
flex: 1;
min-width: 0;
}
.release-item-title {
font-size: var(--text-lg);
font-weight: 700;
text-decoration: none;
color: var(--color-text);
display: block;
margin-bottom: var(--space-1);
}
.release-item-title:hover {
color: var(--color-link);
}
.release-item-meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.release-item-date {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-2);
font-size: var(--text-xs);
color: var(--color-text-muted);
white-space: nowrap;
flex-shrink: 0;
}
/* --- Release notes preview in list --- */
.release-notes-section {
margin-top: var(--space-3);
}
.release-notes-label {
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--space-2);
}
.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;
position: relative;
white-space: pre-wrap;
font-size: var(--text-sm);
color: var(--color-text);
line-height: 1.6;
word-break: break-word;
margin-bottom: var(--space-1);
}
.notes-preview::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2.5em;
background: linear-gradient(transparent, var(--color-bg));
pointer-events: none;
}
.notes-expand[open] .notes-preview {
display: none;
}
.notes-full {
padding-top: var(--space-3);
padding-bottom: var(--space-1);
}
.notes-expand[open] {
display: flex;
flex-direction: column;
}
.notes-expand[open] > summary {
order: 1;
}
.notes-expand[open] > .notes-full {
order: 0;
}
.notes-toggle-label {
display: inline-block;
font-size: var(--text-xs);
color: var(--color-link);
margin-top: var(--space-1);
}
.notes-toggle-label:hover {
text-decoration: underline;
}
.notes-toggle-label::before {
content: "Show more ↓";
}
.notes-expand[open] .notes-toggle-label::before {
content: "Show less ↑";
}
/* --- Release detail --- */
.release-detail {
padding: var(--space-6) 0;
}
.release-header {
margin-bottom: var(--space-6);
}
.release-notes {
margin-bottom: var(--space-6);
}
.release-assets {
margin-bottom: var(--space-6);
}
.release-assets-heading {
font-size: var(--text-base);
font-weight: 600;
margin-bottom: var(--space-3);
}
.asset-list {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.asset-item {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
font-size: var(--text-sm);
}
.asset-item:last-child {
border-bottom: none;
}
.asset-name {
flex: 1;
color: var(--color-link);
text-decoration: none;
font-family: var(--font-mono);
font-size: var(--text-xs);
}
.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;
}
.release-back {
margin-top: var(--space-4);
display: flex;
gap: var(--space-3);
align-items: center;
justify-content: space-between;
}
}
/* ============================================================
@layer utilities
============================================================ */
@layer utilities {
.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;
}
}
/* ============================================================
Pagination
============================================================ */
.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;
}
.pagination {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
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);
}
.ssh-key-info {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-1);
min-width: 0;
}
.ssh-key-name {
font-weight: 500;
}
.ssh-key-fingerprint {
font-family: var(--font-mono);
font-size: var(--text-xs);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.form-textarea {
resize: vertical;
min-height: 4.5rem;
font-family: var(--font-mono);
font-size: var(--text-sm);
}
/* Clone popup */
.clone-popup-details {
position: relative;
display: inline-block;
}
.clone-popup-details > summary {
list-style: none;
}
.clone-popup-details > summary::-webkit-details-marker {
display: none;
}
.clone-popup {
position: absolute;
right: 1rem;
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;
z-index: 100;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.clone-url-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.clone-url-label {
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
min-width: 2.75rem;
}
.clone-url-input {
flex: 1;
font-family: var(--font-mono);
font-size: var(--text-sm);
background: var(--color-bg-subtle);
border: 1px solid var(--color-border-muted);
border-radius: var(--radius-md);
padding: var(--space-1) var(--space-2);
color: var(--color-text);
cursor: text;
min-width: 0;
}
.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);
}
▾Msrc/views/issues/IssueDetail.tsx
@@ -56,6 +56,20 @@ export function IssueDetail({
</span>
{canEditIssue && (
<div class="issue-detail-meta-actions">
{user?.isAdmin && issue.status === "open" && (
<form
method="POST"
action={`/${repo.name}/issues/${issue.number}/complete`}
class="inline-form"
>
<button
type="submit"
class="btn btn-sm btn-primary"
>
Completed
</button>
</form>
)}
{user?.isAdmin && (
<form
method="POST"
▾Msrc/views/issues/IssueList.tsx
@@ -1,6 +1,7 @@
import type { IssueRow, RepositoryRow } from "../../db/index.ts";
import { displayName } from "../../lib/users.ts";
import type { SessionUser } from "../../middleware/session.ts";
import { Avatar } from "../Avatar.tsx";
import { Layout } from "../layout.tsx";
import { Pagination, type PaginationInfo } from "../Pagination.tsx";
import { RepoHeader } from "../repos/RepoHeader.tsx";
@@ -9,8 +10,8 @@ import { RepoNav } from "../repos/RepoNav.tsx";
interface IssueListProps {
user: SessionUser | null;
repo: RepositoryRow;
issues: (IssueRow & { author_username: string })[];
status: "open" | "closed";
issues: (IssueRow & { author_username: string; author_avatar_version: number | null })[];
status: "open" | "closed" | "completed";
counts: Record<string, number>;
pagination: PaginationInfo;
}
@@ -39,6 +40,17 @@ export function IssueList({
<span class="tab-count">{counts.open}</span>
)}
</a>
<a
href={`/${repo.name}/issues?status=completed`}
class={`list-tab${status === "completed" ? " active" : ""}`}
>
Completed{" "}
{(counts.completed ?? 0) > 0 && (
<span class="tab-count">
{counts.completed}
</span>
)}
</a>
<a
href={`/${repo.name}/issues?status=closed`}
class={`list-tab${status === "closed" ? " active" : ""}`}
@@ -81,6 +93,11 @@ export function IssueList({
</span>
</div>
<div class="issue-meta">
<Avatar
userId={issue.author_id}
version={issue.author_avatar_version}
size={20}
/>
<span class="issue-author">
opened by{" "}
{displayName(issue.author_username)}
▾Msrc/views/patches/PatchDetail.tsx
@@ -250,16 +250,18 @@ export function PatchDetail({
<div
class={`apply-result apply-${applyResult.status}`}
>
<span class="apply-icon">
{applyResult.status === "clean"
? "✓"
: "✗"}
</span>
<span>
{applyResult.status === "clean"
? "Applies cleanly"
: "Has conflicts"}
</span>
<div>
<span class="apply-icon">
{applyResult.status === "clean"
? "✓"
: "✗"}
</span>
<span>
{applyResult.status === "clean"
? "Applies cleanly"
: "Has conflicts"}
</span>
</div>
{applyResult.output && (
<pre class="apply-output">
{applyResult.output}
▾Msrc/views/patches/PatchList.tsx
@@ -1,6 +1,7 @@
import type { PatchRow, RepositoryRow } from "../../db/index.ts";
import { displayName } from "../../lib/users.ts";
import type { SessionUser } from "../../middleware/session.ts";
import { Avatar } from "../Avatar.tsx";
import { Layout } from "../layout.tsx";
import { Pagination, type PaginationInfo } from "../Pagination.tsx";
import { RepoHeader } from "../repos/RepoHeader.tsx";
@@ -9,7 +10,7 @@ import { RepoNav } from "../repos/RepoNav.tsx";
interface PatchListProps {
user: SessionUser | null;
repo: RepositoryRow;
patches: (PatchRow & { author_username: string })[];
patches: (PatchRow & { author_username: string; author_avatar_version: number | null })[];
status: string;
counts: Record<string, number>;
pagination: PaginationInfo;
@@ -90,6 +91,11 @@ export function PatchList({
</span>
</div>
<div class="issue-meta">
<Avatar
userId={patch.author_id}
version={patch.author_avatar_version}
size={20}
/>
<span>
by {displayName(patch.author_username)}
</span>
▾Msrc/views/releases/ReleaseDetail.tsx
@@ -126,17 +126,24 @@ export function ReleaseDetail({
["tar.gz", ".tar.gz"],
["tar.zst", ".tar.zst"],
] as const
).map(([format, ext]) => (
<li class="asset-item asset-item-pending">
<span class="asset-name asset-name-pending">
{`${repo.name}-${release.commit_hash.slice(0, 8)}${ext}`}
</span>
<span class="asset-meta">
Source code ({format}) —
generating…
</span>
</li>
))}
)
.filter(
([format]) =>
!sourceArchives.some(
(a) => a.format === format,
),
)
.map(([format, ext]) => (
<li class="asset-item asset-item-pending">
<span class="asset-name asset-name-pending">
{`${repo.name}-${release.commit_hash.slice(0, 8)}${ext}`}
</span>
<span class="asset-meta">
Source code ({format}) —
generating…
</span>
</li>
))}
</ul>
</div>
)}
▾Msrc/views/repos/RepoHome.tsx
@@ -1,4 +1,4 @@
import { BASE_URL, SSH_PORT } from "../../config.ts";
import { BASE_URL, SSH_DISABLED, SSH_PORT } from "../../config.ts";
import type { RepositoryRow } from "../../db/index.ts";
import type { SessionUser } from "../../middleware/session.ts";
import type { TreeEntry } from "../../services/git.ts";
@@ -71,15 +71,19 @@ git push origin main`}</code>
class="clone-url-input"
/>
</div>
<div class="clone-url-row">
<span class="clone-url-label">SSH</span>
<input
type="text"
readonly
value={sshUrl(repo.name)}
class="clone-url-input"
/>
</div>
{!SSH_DISABLED && (
<div class="clone-url-row">
<span class="clone-url-label">
SSH
</span>
<input
type="text"
readonly
value={sshUrl(repo.name)}
class="clone-url-input"
/>
</div>
)}
</div>
</details>
</div>
▾Msrc/views/repos/RepoList.tsx
@@ -44,14 +44,7 @@ export function RepoList({ user, repos, search, pagination }: RepoListProps) {
No repositories match <strong>{search}</strong>.
</p>
) : (
<>
<p>No repositories yet.</p>
{user?.isAdmin && (
<a href="/new" class="btn btn-primary">
Create your first repository
</a>
)}
</>
<p>No repositories yet.</p>
)}
</div>
) : (