mirror of
https://github.com/lutinglt/gitea-github-theme.git
synced 2026-10-28 09:44:55 +00:00
607 lines
31 KiB
Markdown
607 lines
31 KiB
Markdown
# Architecture: Gitea GitHub Theme
|
||
|
||
## Project Overview
|
||
|
||
Gitea GitHub Theme is a build-time CSS generation framework that produces static `.css` theme files for the
|
||
[Gitea](https://github.com/go-gitea/gitea) self-hosted code platform. The project takes design tokens and color
|
||
definitions as input, transforms them through a layered pipeline, and outputs 30+ minified CSS files covering 7 theme
|
||
series (default, colorblind, tritanopia, pink, gitea, catppuccin, high-contrast).
|
||
|
||
**Core principle:** Full type safety and CSS-in-JS developer experience at build time, pure static CSS delivered to the
|
||
browser with zero runtime overhead.
|
||
|
||
---
|
||
|
||
## Architecture Layers
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ Theme Configuration Layer │
|
||
│ theme.config.ts: declares theme series, color types, │
|
||
│ display names, accessibility metadata │
|
||
└──────────────────────────┬───────────────────────────────────┘
|
||
│
|
||
┌──────────────────────────▼───────────────────────────────────┐
|
||
│ Color Definition Layer │
|
||
│ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │
|
||
│ │ primer/ │ │ themes/ │ │ themes/catppuccin │ │
|
||
│ │ (auto- │ │ github.ts │ │ .ts │ │
|
||
│ │ generated│ │ gitea*.ts │ │ │ │
|
||
│ │ Primer │ │ │ │ │ │
|
||
│ │ tokens) │ │ │ │ │ │
|
||
│ └────┬─────┘ └──────┬───────┘ └─────────┬─────────┘ │
|
||
│ │ │ │ │
|
||
│ primer2GitHubColor defineTheme catppuccin2ThemeColor │
|
||
│ │ │ │ │
|
||
│ ▼ ▼ ▼ │
|
||
│ GitHubColor Theme ThemeColor │
|
||
│ (colorType: │
|
||
│ "github") │
|
||
└──────────────────────────┬───────────────────────────────────┘
|
||
│
|
||
┌──────────────────────────▼───────────────────────────────────┐
|
||
│ Color Transformation Layer │
|
||
│ src/palette/ │
|
||
│ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │
|
||
│ │ github.ts │ │ theme.ts │ │ gitea.ts │ │
|
||
│ │ GitHubColor│ │ ThemeColor │ │ GiteaColor │ │
|
||
│ │ → │ │ → │ │ → │ │
|
||
│ │ ThemeColor │ │ ThemeVars │ │ ThemeVars │ │
|
||
│ └────────────┘ └────────────┘ └──────────────────┘ │
|
||
│ │
|
||
│ Unified output: ThemeVars (flat key-value map of CSS vars) │
|
||
└──────────────────────────┬───────────────────────────────────┘
|
||
│
|
||
┌──────────────────────────▼───────────────────────────────────┐
|
||
│ Core Framework Layer │
|
||
│ src/core/ │
|
||
│ ┌──────────────┐ ┌──────────┐ ┌───────────────────┐ │
|
||
│ │ theme.ts │ │ css.ts │ │ chroma.ts │ │
|
||
│ │ createTheme()│ │ css() │ │ codemirror.ts │ │
|
||
│ │ defineTheme()│ │cssCombine│ │ meta.ts │ │
|
||
│ └──────┬───────┘ │cssStyle()│ └───────────────────┘ │
|
||
│ │ └────┬─────┘ │
|
||
│ │ │ │
|
||
│ vanilla-extract adapter (src/vanilla-extract.ts) │
|
||
│ ┌──────────────────────────────────────────────────┐ │
|
||
│ │ setAdapter() → intercepts appendCss, │ │
|
||
│ │ registerClassName, registerComposition │ │
|
||
│ │ → serializes all CSS to strings at build time │ │
|
||
│ └──────────────────────────────────────────────────┘ │
|
||
└──────────────────────────┬───────────────────────────────────┘
|
||
│
|
||
┌──────────────────────────▼───────────────────────────────────┐
|
||
│ Style Override Layer │
|
||
│ styles/ │
|
||
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||
│ │ public/ │ │ components/ │ │ templates/ │ │
|
||
│ │ (base │ │ (page-level │ │ (template- │ │
|
||
│ │ elements│ │ styles) │ │ specific styles) │ │
|
||
│ └──────────┘ └──────────────┘ └──────────────────┘ │
|
||
│ common/ (reusable CSS fragments: buttons, labels, anims) │
|
||
│ │
|
||
│ Each file uses css`...` template literals │
|
||
│ → composed with cssCombine() at export boundary │
|
||
└──────────────────────────┬───────────────────────────────────┘
|
||
│
|
||
┌──────────────────────────▼───────────────────────────────────┐
|
||
│ Build Pipeline │
|
||
│ src/vite-plugin/index.ts (Vite plugin) │
|
||
│ │
|
||
│ config() hook: │
|
||
│ 1. Dynamically import theme.config.ts │
|
||
│ 2. Resolve all theme entries + generate auto themes │
|
||
│ │
|
||
│ generateBundle() hook: │
|
||
│ 1. Import styles/ → CSS string (public + components) │
|
||
│ 2. For each theme entry: │
|
||
│ a. compileCSS(createThemeMetaInfo + createTheme) │
|
||
│ b. lightningcss.transform() → minify │
|
||
│ c. Append style overrides (non-auto themes only) │
|
||
│ d. emitFile → output .css asset │
|
||
│ 3. For auto themes: generate @import declarations │
|
||
└──────────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
dist/theme-github-*.css
|
||
```
|
||
|
||
---
|
||
|
||
## Core Design Patterns
|
||
|
||
### 1. Vanilla-Extract Adapter Hijacking
|
||
|
||
**Problem solved:** Vanilla-extract is a zero-runtime CSS-in-JS library that generates static CSS files at build time
|
||
via its Vite/esbuild plugin. However, Gitea themes need single-file CSS output and programmatic control over the theme
|
||
generation process (one output file per theme, auto theme generation, etc.).
|
||
|
||
**Solution:** Bypass vanilla-extract's default plugin and use its low-level adapter API directly.
|
||
|
||
In [src/vanilla-extract.ts](src/vanilla-extract.ts):
|
||
|
||
```typescript
|
||
export function compileCSS(compileFn: () => void): string {
|
||
// 1. Install custom adapter to capture CSS objects into arrays
|
||
setAdapter({
|
||
appendCss: css => capturedCssObjs.push(css),
|
||
registerClassName: className => localClassNames.add(className),
|
||
registerComposition: composition => composedClassLists.push(composition),
|
||
// ... other callbacks implemented as no-ops
|
||
});
|
||
|
||
// 2. Set file scope for debugging
|
||
setFileScope(getCallerInfo(1));
|
||
|
||
// 3. Execute the caller's vanilla-extract API calls
|
||
compileFn();
|
||
|
||
// 4. Tear down adapter
|
||
endFileScope();
|
||
removeAdapter();
|
||
|
||
// 5. Serialize captured CSS objects to strings
|
||
return transformCss({
|
||
localClassNames: Array.from(localClassNames),
|
||
composedClassLists,
|
||
cssObjs: capturedCssObjs,
|
||
}).join("\n");
|
||
}
|
||
```
|
||
|
||
**How it works:**
|
||
|
||
1. `compileCSS` wraps a synchronous function that calls vanilla-extract APIs (`createGlobalTheme`, `globalStyle`,
|
||
`globalKeyframes`, etc.)
|
||
2. The custom adapter intercepts all CSS generation calls, capturing them into in-memory arrays
|
||
3. After the function executes, `transformCss()` serializes the captured objects into CSS strings
|
||
4. The adapter is torn down, leaving no global side effects
|
||
|
||
**Safety guarantee:** Each `compileCSS` call is fully self-contained: install → capture → serialize → teardown. No state
|
||
leaks between calls.
|
||
|
||
**Key insight:** Vanilla-extract's `createGlobalThemeContract` + `createGlobalTheme` fit this pattern perfectly—they
|
||
only produce CSS during `createGlobalTheme` calls, which happen exactly within our capture scope.
|
||
|
||
### 2. Three Color Input Pipelines
|
||
|
||
**Problem solved:** Different color theme sources have different data structures. Primer design tokens (from GitHub)
|
||
have hundreds of nested keys. Catppuccin has its own palette structure. Gitea's native format uses a flat structure. All
|
||
three need to be supported.
|
||
|
||
**Solution:** Three conversion paths, all converging on `ThemeVars`:
|
||
|
||
```
|
||
Primer tokens ──→ GitHubColor ──→ ThemeColor ──→ ThemeVars
|
||
Catppuccin flavor ──────────────→ ThemeColor ──→ ThemeVars
|
||
GiteaColor ────────────────────────────────────→ ThemeVars
|
||
```
|
||
|
||
Each path has different complexity requirements:
|
||
|
||
| Path | Use case | Complexity | Examples |
|
||
| -------- | ---------------------------- | -------------------------------- | ---------------------------------- |
|
||
| `github` | Full Primer token compliance | High (hundreds of mapped keys) | Default, colorblind, high-contrast |
|
||
| `theme` | 15 base colors, custom theme | Low (auto-derives 200+ CSS vars) | Catppuccin, community themes |
|
||
| `gitea` | Gitea native color format | Medium (direct mapping) | Gitea-compatible themes |
|
||
|
||
**Design rationale:** The `theme` path is the "onboarding path"—contributors only need to define 15 semantic colors
|
||
(`red`, `orange`, `yellow`, …, `white`) plus primary and secondary. The framework automatically computes:
|
||
|
||
- 7-level lightness variants (dark-1 through dark-7, light-1 through light-7)
|
||
- 9-level alpha variants (alpha-10 through alpha-90)
|
||
- Badge colors and their hover states
|
||
- Message colors (error/success/warning/info)
|
||
- ANSI console colors
|
||
- Direction-aware (scale direction reversed for dark/light themes)
|
||
|
||
Implemented in `theme2ThemeVars()` in [src/palette/theme.ts](src/palette/theme.ts).
|
||
|
||
### 3. CSS Variable Naming Convention
|
||
|
||
**Problem solved:** Gitea expects specific CSS variable naming patterns (`--color-primary-dark-1`,
|
||
`--color-red-badge-bg`, etc.). We need type-safe access in TypeScript (`themeVars.color.primary.dark.num1`).
|
||
|
||
**Solution:** Use vanilla-extract's `createGlobalThemeContract` with a custom `varMapper`.
|
||
|
||
In [src/types/vars.ts](src/types/vars.ts):
|
||
|
||
```typescript
|
||
function varMapper(prefix: string | null = null) {
|
||
return (value: string | null, path: string[]) => {
|
||
if (value === null) {
|
||
// Auto-generate name from path: ["color","primary","dark","num1"] → "color-primary-dark-1"
|
||
path = path.filter(item => item !== "self"); // remove "self" key
|
||
path = path.map(item => item.replace(/^num/, "")); // strip "num" prefix
|
||
value = path.join("-");
|
||
}
|
||
if (prefix) value = `${prefix}-${value}`;
|
||
return value;
|
||
};
|
||
}
|
||
```
|
||
|
||
This creates a bidirectional mapping:
|
||
|
||
| TypeScript access | CSS variable |
|
||
| ---------------------------------------------- | -------------------------------------- |
|
||
| `themeVars.color.primary.self` | `--color-primary` |
|
||
| `themeVars.color.primary.dark.num1` | `--color-primary-dark-1` |
|
||
| `themeVars.github.button.primary.bgColor.rest` | `--github-button-primary-bgColor-rest` |
|
||
| `syntaxVars.keyword` | `--color-syntax-keyword` |
|
||
| `customThemeVars.cloneMenuWidth` | `--custom-clone-menu-width` |
|
||
|
||
### 4. Build-Time CSS Syntax Validation
|
||
|
||
**Problem solved:** CSS in template literals can contain syntax errors that only surface at browser runtime.
|
||
|
||
**Solution:** The `css()` template tag validates CSS syntax at build time using lightningcss.
|
||
|
||
In [src/core/css.ts](src/core/css.ts):
|
||
|
||
```typescript
|
||
export function css(strings: TemplateStringsArray, ...values: CSSInterpolation[]): CSSString {
|
||
const result = strings.reduce((acc, s, i) => acc + s + String(values[i] ?? ""), "");
|
||
// Build-time validation: lightningcss parses and reports syntax errors
|
||
transform({ filename: getCallerInfo(1), code: Buffer.from(result) });
|
||
return result as CSSString;
|
||
}
|
||
```
|
||
|
||
If a developer writes invalid CSS, the build fails immediately with `file:line` info pointing to the problematic
|
||
template literal. This catches errors that would otherwise only be discovered through browser DevTools.
|
||
|
||
**Trade-off:** Each `css` template call invokes `transform()` once. For ~60 style files, this adds ~200ms to the initial
|
||
build (one-time cost, subsequent builds benefit from Bun's module cache).
|
||
|
||
---
|
||
|
||
## Vite Build Pipeline
|
||
|
||
The build is orchestrated by a custom Vite plugin in [src/vite-plugin/index.ts](src/vite-plugin/index.ts):
|
||
|
||
### Phase 1: Configuration Resolution (`config` hook)
|
||
|
||
1. Dynamically import `theme.config.ts` via Bun native TypeScript + package exports self-reference
|
||
(`@lutinglt/gitea-github-theme/theme.config.ts`)
|
||
2. Iterate over each theme series, generating entries for each theme variant (dark, light, soft-dark, frappe, macchiato,
|
||
etc.)
|
||
3. When a series has both dark and light variants, auto-generate an `auto` theme entry
|
||
4. Provide a virtual module entry (Vite needs at least one input to trigger `generateBundle`)
|
||
|
||
### Phase 2: CSS Generation (`generateBundle` hook)
|
||
|
||
For each **non-auto theme**:
|
||
|
||
1. Call `compileCSS()` with a function that:
|
||
- Creates Gitea theme meta info (`createThemeMetaInfo`) — sets `--theme-color-scheme`, `--theme-display-name`,
|
||
`--theme-colorblind-type`
|
||
- Creates theme CSS variables (`createTheme`) — generates all `--color-*` and `--github-*` variables, global styles,
|
||
keyframe animations, code highlighting
|
||
2. Minify captured CSS via `lightningcss.transform()`
|
||
3. Append public style overrides (`styles/`)
|
||
|
||
For each **auto theme**:
|
||
|
||
1. Generate `@import` declarations with `prefers-color-scheme` media queries referencing the corresponding dark/light
|
||
theme files
|
||
2. Include theme meta info so Gitea recognizes it as an auto color theme
|
||
|
||
### CSS Output Structure
|
||
|
||
```
|
||
theme-github-dark.css:
|
||
├── @keyframes (overlayAppear, overlayAppearDown, overlayAppearUp)
|
||
├── gitea-theme-meta-info { --theme-color-scheme: dark; ... }
|
||
├── :root { --theme-version: "1.26.2.260605"; --is-dark-theme: true; ... }
|
||
├── :root { --color-primary: #4493f8; --color-primary-contrast: ... }
|
||
├── ... (200+ CSS variable declarations)
|
||
├── .chroma { ... } (syntax highlighting)
|
||
├── .code-editor-container { ... } (CodeMirror highlighting)
|
||
└── <public style overrides> (~60KB single-line minified)
|
||
|
||
theme-github-auto.css:
|
||
├── @import "./theme-github-dark.css" (prefers-color-scheme: dark);
|
||
├── @import "./theme-github-light.css" (prefers-color-scheme: light);
|
||
└── gitea-theme-meta-info { --theme-color-scheme: auto; ... }
|
||
```
|
||
|
||
---
|
||
|
||
## Color System Deep Dive
|
||
|
||
### Color Scale Calculation
|
||
|
||
`scaleColorLight()` in [src/functions/color.ts](src/functions/color.ts) implements the Sass `color.scale()` algorithm:
|
||
|
||
```
|
||
darken: newL = currentL × (1 + scale/100) [scale < 0]
|
||
lighten: newL = currentL + (1 - currentL) × (scale/100) [scale > 0]
|
||
```
|
||
|
||
This is **proportional scaling**, not linear offset. Its significance:
|
||
|
||
- `color.scale(#000000, $lightness: 50%)` → 50% gray (not "50% darker than black", which is still black)
|
||
- `color.scale(#ffffff, $lightness: -50%)` → 50% gray (not "50% lighter than white", which is still white)
|
||
- The proportional approach ensures visually meaningful changes at both brightness extremes
|
||
|
||
### ThemeColor → ThemeVars Derivation
|
||
|
||
`theme2ThemeVars()` in [src/palette/theme.ts](src/palette/theme.ts) takes `ThemeColor` (15 base colors + metadata) and
|
||
derives the full `ThemeVars` (200+ CSS variable values):
|
||
|
||
1. **Primary scale**: 7 dark variants, 7 light variants, 9 alpha variants, hover/active states — all from a single hex
|
||
color
|
||
2. **Secondary scale**: 13 dark variants, 4 light variants, 9 alpha variants, button hover/active states
|
||
3. **Named colors** (13 of 15 base colors): each generates self/light/dark-1/dark-2/badge-self/badge-bg/badge-hover-bg
|
||
4. **Semantic message colors**: bg/border/text with rgba transparency and saturation adjustments
|
||
5. **ANSI colors**: 16-color palette mapped from named colors
|
||
6. **Direction-aware**: `isDarkTheme` flag reverses scale direction — "dark" variants in dark themes go toward lighter
|
||
values (to maintain contrast)
|
||
|
||
---
|
||
|
||
## Style Override Strategy
|
||
|
||
### Selector Priority Hierarchy
|
||
|
||
Style overrides use Gitea's existing DOM selectors. The override strategy follows a priority ladder:
|
||
|
||
1. **Color variables** — All colors reference CSS variables from the theme (never hardcoded hex values)
|
||
2. **Element-level overrides** — Base elements like `.ui.button`, `#navbar`, `input`
|
||
3. **Component-level overrides** — Page-specific areas: heatmap, issues, actions, repo pages
|
||
4. **`!important` escape hatch** — Only used when Gitea sets colors via inline `style` attributes (e.g., heatmap
|
||
contribution squares)
|
||
|
||
### cssCombine() Explicit Composition
|
||
|
||
Styles are composed together via a `cssCombine()` call tree:
|
||
|
||
```
|
||
styles/index.ts
|
||
├── public/index.ts (base elements, buttons, inputs, dropdowns, menus, tables)
|
||
├── components/index.ts (navbar, heatmap, issues, repo files, Actions, etc.)
|
||
└── templates/index.ts (template-specific styles)
|
||
```
|
||
|
||
Each leaf module exports `default cssCombine(...)`, chaining CSS fragments together. This explicit composition makes the
|
||
dependency graph visible and avoids the implicit ordering issues of CSS `@import` or `@use`.
|
||
|
||
### cssStyle() Reusable Fragments
|
||
|
||
`cssStyle()` in [src/core/css.ts](src/core/css.ts) enables extracting reusable CSS property sets while retaining
|
||
vanilla-extract's type checking:
|
||
|
||
```typescript
|
||
// In styles/common/button.ts:
|
||
export const primaryButtonStyle = cssStyle({
|
||
color: themeVars.github.button.primary.fgColor.rest,
|
||
backgroundColor: themeVars.github.button.primary.bgColor.rest,
|
||
borderColor: themeVars.github.button.primary.borderColor.rest,
|
||
boxShadow: themeVars.github.shadow.resting.small,
|
||
});
|
||
|
||
// In styles/public/button.ts:
|
||
const baseButton = css`
|
||
.ui.primary.button {
|
||
${primaryButtonStyle}
|
||
}
|
||
`;
|
||
```
|
||
|
||
This preserves type safety (vanilla-extract validates property names and value types) in a system that ultimately
|
||
outputs static CSS strings.
|
||
|
||
---
|
||
|
||
## Directory Structure
|
||
|
||
```
|
||
├── theme.config.ts # Theme series declaration (framework entry point)
|
||
├── src/
|
||
│ ├── index.ts # Public API exports
|
||
│ ├── core/ # Core framework
|
||
│ │ ├── theme.ts # createTheme, defineTheme (color → CSS variables)
|
||
│ │ ├── css.ts # css(), cssCombine(), cssStyle()
|
||
│ │ ├── chroma.ts # Chroma (server-side) syntax highlighting
|
||
│ │ ├── codemirror.ts # CodeMirror (client-side) syntax highlighting
|
||
│ │ ├── meta.ts # Gitea theme meta info (color scheme, display name)
|
||
│ │ └── types.d.ts # MapLeafNodes, CSSVarFunction, Tokens
|
||
│ ├── palette/ # Color conversion and transformation
|
||
│ │ ├── theme.ts # ThemeColor → ThemeVars (simplified API)
|
||
│ │ ├── github.ts # GitHubColor → ThemeColor (Primer path)
|
||
│ │ ├── gitea.ts # GiteaColor → ThemeVars (native path)
|
||
│ │ ├── catppuccin.ts # CatppuccinFlavor → ThemeColor
|
||
│ │ ├── primer.ts # Primer tokens → GitHubColor / Chroma / CodeMirror
|
||
│ │ ├── display.ts # Display color scale → GitHubColor overrides
|
||
│ │ └── catppuccinSyntax.ts # Catppuccin syntax highlighting
|
||
│ ├── functions/ # Utility functions
|
||
│ │ ├── color.ts # scaleColorLight (Sass color.scale) + rgba wrapper
|
||
│ │ └── var.ts # extractVarName (CSS variable name extraction)
|
||
│ ├── types/ # Type definitions and CSS variable contracts
|
||
│ │ ├── color/ # Color structure definitions (primary, secondary, named, etc.)
|
||
│ │ └── vars.ts # createGlobalThemeContract + varMapper
|
||
│ ├── vite-plugin/ # Build tooling
|
||
│ │ ├── index.ts # Main Vite plugin
|
||
│ │ ├── themeConfig.ts # defineThemeConfig, TypeScript types
|
||
│ │ └── utils.ts # buildFullThemeName, buildFullDisplayName
|
||
│ ├── vanilla-extract.ts # Adapter hijacking (compileCSS, captureStyle)
|
||
│ ├── selectors.ts # Shared CSS selectors (root, chroma, codeMirror, emoji)
|
||
│ └── utils.ts # deepOverride, getCallerInfo
|
||
├── styles/ # CSS style overrides
|
||
│ ├── common/ # Reusable CSS fragments (buttons, labels, animations)
|
||
│ ├── public/ # Base elements (buttons, inputs, dropdowns, menus, tables)
|
||
│ ├── components/ # Page-specific styles (navbar, heatmap, repo, issues, Actions)
|
||
│ └── templates/ # Template-specific styles
|
||
├── themes/ # Color theme definitions
|
||
│ ├── github.ts # GitHub theme colors (from Primer tokens)
|
||
│ ├── giteaDark.ts # Gitea dark theme colors (native format)
|
||
│ ├── giteaLight.ts # Gitea light theme colors (native format)
|
||
│ └── catppuccin.ts # Catppuccin theme colors
|
||
├── primer/ # Auto-generated Primer design tokens (from @primer/primitives)
|
||
├── scripts/ # Build and utility scripts
|
||
│ ├── buildPrimerTSToken.ts # Generate primer/ TypeScript tokens from @primer/primitives
|
||
│ ├── syncTheme.ts # Sync build artifacts to remote Gitea instance
|
||
│ ├── updateGitea.ts # Update Gitea reference files
|
||
│ ├── translate.ts # Generate localization/translation files
|
||
│ └── version.ts # Version management
|
||
├── dist/ # Build output (30+ CSS files)
|
||
└── templates/ # Optional Gitea Go template overrides
|
||
```
|
||
|
||
---
|
||
|
||
## Key Design Decisions & Trade-offs
|
||
|
||
### Decision 1: Vanilla-Extract for CSS authoring only, not runtime
|
||
|
||
**Choice:** Use vanilla-extract's `createGlobalTheme`/`createGlobalThemeContract` for type-safe CSS variable generation,
|
||
but serialize to static CSS strings at build time via adapter hijacking.
|
||
|
||
**Rejected:** Using vanilla-extract's built-in Vite plugin (outputs per-module CSS files, unsuitable for single-file
|
||
theme output).
|
||
|
||
**Trade-off:** Adapter API is undocumented and may change between vanilla-extract versions. Mitigated by locking
|
||
`@vanilla-extract/css` version and using snapshot tests to detect CSS output changes.
|
||
|
||
### Decision 2: Single-file CSS output
|
||
|
||
**Choice:** Each theme is a single CSS file containing all variables and all style overrides.
|
||
|
||
**Rejected:** Splitting into multiple CSS files via `@import` chains (Gitea's theme system loads one CSS file per
|
||
theme).
|
||
|
||
**Trade-off:** Style overrides are duplicated across all theme files (~60KB × 30 themes ≈ 1.8MB redundancy). This is a
|
||
Gitea constraint, not a design choice. The alternative (separate common.css) would require Gitea to support multi-file
|
||
theme loading.
|
||
|
||
### Decision 3: LightningCSS for both syntax validation and output minification
|
||
|
||
**Choice:** Use LightningCSS for build-time CSS syntax validation (in `css()` template tag) and output minification (in
|
||
Vite plugin).
|
||
|
||
**Rejected:** PostCSS (slower), esbuild CSS (less feature-complete), no validation at all.
|
||
|
||
**Trade-off:** LightningCSS is lenient—it recovers from many syntax errors rather than throwing. This means validation
|
||
in `css()` catches severe errors but may silently accept certain edge cases. Build output snapshot tests partially
|
||
compensate for this.
|
||
|
||
### Decision 4: Bun-only runtime
|
||
|
||
**Choice:** Require Bun ≥ 1.3.14 for development and building.
|
||
|
||
**Rejected:** Cross-runtime support (Node + Bun).
|
||
|
||
**Trade-off:** Limits contributor environments. Benefits: native TypeScript execution (no ts-node/esbuild overhead),
|
||
consistent stack trace format (for debugging utilities), faster builds (Bun native bundler).
|
||
|
||
### Decision 5: Package self-referencing for internal imports
|
||
|
||
**Choice:** Use `@lutinglt/gitea-github-theme/core` style imports even within the same package, via `exports` mappings
|
||
in `package.json`.
|
||
|
||
**Rejected:** Relative path imports everywhere.
|
||
|
||
**Trade-off:** Requires `paths` mappings in `tsconfig.json` for IDE support. Benefits: import paths are consistent
|
||
whether used as a dependency or developed locally; no path confusion between `src/`, `styles/`, and `themes/`
|
||
directories.
|
||
|
||
---
|
||
|
||
## Adding a New Theme
|
||
|
||
### Quick Path (15 color definitions)
|
||
|
||
1. Create `themes/my-theme.ts`:
|
||
|
||
```typescript
|
||
import { defineTheme } from "@lutinglt/gitea-github-theme/core";
|
||
import type { ThemeColor, Syntax } from "@lutinglt/gitea-github-theme/core";
|
||
|
||
const themeColor: ThemeColor = {
|
||
isDarkTheme: true,
|
||
primary: "#8b5cf6", // accent color (purple)
|
||
primaryContrast: "#ffffff",
|
||
secondary: "#27272a", // border color
|
||
base: {
|
||
red: "#ef4444",
|
||
orange: "#f97316",
|
||
yellow: "#eab308",
|
||
olive: "#84cc16",
|
||
green: "#22c55e",
|
||
teal: "#14b8a6",
|
||
cyan: "#06b6d4",
|
||
blue: "#3b82f6",
|
||
violet: "#8b5cf6",
|
||
purple: "#a855f7",
|
||
pink: "#ec4899",
|
||
brown: "#a16207",
|
||
black: "#18181b",
|
||
grey: "#71717a",
|
||
gold: "#eab308",
|
||
white: "#fafafa",
|
||
},
|
||
console: {
|
||
/* Actions log colors */
|
||
},
|
||
diff: {
|
||
/* Code diff colors */
|
||
},
|
||
other: {
|
||
/* body, text, input, shadow, etc. */
|
||
},
|
||
github: {
|
||
/* GitHub-specific variables */
|
||
},
|
||
};
|
||
|
||
const syntaxColor: Syntax = {
|
||
/* Code highlighting colors */
|
||
};
|
||
|
||
export default defineTheme({ colorType: "theme", themeColor, syntaxColor });
|
||
```
|
||
|
||
2. Register in `theme.config.ts`:
|
||
|
||
```typescript
|
||
{
|
||
themeSeriesName: "my-theme",
|
||
themes: {
|
||
dark: { theme: myDarkTheme },
|
||
// Optional: add light variant for auto theme generation
|
||
light: { theme: myLightTheme },
|
||
},
|
||
}
|
||
```
|
||
|
||
3. Run `bun bundle` — the theme CSS will appear in `dist/`.
|
||
|
||
### Full Path (Primer tokens)
|
||
|
||
For pixel-perfect GitHub color matching, use the `github` path with direct Primer design token mapping. See
|
||
`themes/github.ts` and `src/palette/github.ts` for the full mapping reference.
|
||
|
||
---
|
||
|
||
## Testing Strategy
|
||
|
||
| Layer | Test type | Location | What it verifies |
|
||
| ------------ | --------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||
| Utility | Unit tests | `src/utils.test.ts` | `deepOverride` behavior, `getCallerInfo` stack parsing |
|
||
| Functions | Unit tests | `src/functions/*.test.ts` | `scaleColorLight` numeric computation, `extractVarName` parsing |
|
||
| Core | Unit tests | `src/core/css.test.ts` | `css()` validation, `cssCombine()` composition, `cssStyle()` serialization |
|
||
| Palette | Snapshot tests | `src/palette/*.test.ts` | Full color conversion chains (Primer → ThemeColor → ThemeVars, Catppuccin → ThemeColor → ThemeVars, etc.) |
|
||
| Palette | Structure tests | `src/palette/github.test.ts` | Color value validity (all leaf nodes are valid CSS color strings), required keys exist |
|
||
| Build output | Integration | `src/build-output.test.ts` | Expected files exist, CSS content validity (vars present, no debug residue, hex lowercase, color-scheme match), file size limits |
|
||
|
||
---
|
||
|
||
## Related Documents
|
||
|
||
- [CONTRIBUTING.md](CONTRIBUTING.md) — Development environment setup and contribution guide
|
||
- [README.md](README.md) — User-facing installation and usage
|
||
- [README_CN.md](README_CN.md) — Chinese user installation and usage
|
||
- [primer/README.md](primer/README.md) — Auto-generated Primer token documentation
|
||
- [CHANGELOG.md](CHANGELOG.md) — Release history
|