diff --git a/.gitattributes b/.gitattributes index 1e4e0f9b..267a2c28 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,7 +4,6 @@ .github/** linguist-vendored .vscode/** linguist-generated dist/** linguist-generated -gitea/** linguist-generated primer/** linguist-generated primer/index.ts -linguist-generated src/**/__snapshots__/** linguist-generated diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f205a30a..85dce3ad 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,239 +1,99 @@ -# 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. - ---- - -## Forgejo component assets - -Forgejo-specific UI components are colocated under `components/`. Each component directory keeps its template fragments, -CSS, browser controller, and focused tests together. `components/common` contains controls that do not know about page -routes; `components/project` contains the project list shared by repository, organization and user pages; -`components/repository` contains repository filtering, sorting, and result rendering. Page composition remains in -`templates/org`, `templates/user`, and `templates/repo`, while page-only CSS lives under `styles/pages`. - -The bundle step assembles this source tree into `dist/forgejo/templates` and `dist/forgejo/public/assets`, which are the -installable Forgejo paths. The local preview mounts that assembled tree, so the same output is exercised during -development and packaging. - -## 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(); +# Architecture + +Forgejo GitHub Theme makes Forgejo 17 look like GitHub. It ships two parts, which admins install separately: + +- **Theme stylesheets**, `dist/theme-github-*.css`, one per color theme. Forgejo loads the stylesheet of the theme a + user selects. Each holds the theme's color variables and the theme-wide overrides of Forgejo's own markup, so it works + without the templates. +- **The template pack**, `dist/forgejo`: Forgejo template overrides and the stylesheets and scripts they load. It + changes Forgejo's markup where styles alone cannot reach GitHub's layout, and it is bound to the Forgejo release it + was written against (17.0.0). + +`bun bundle` builds both. Vite generates the theme stylesheets, then `scripts/assembleForgejo.ts` assembles the template +pack. The local preview and the release archives use the same `dist/forgejo` tree. + +```text +theme.config.ts, themes/, primer/, styles/*.ts templates/, components/, styles/pages/, public/ + │ │ + ▼ src/vite-plugin (vite build) │ + dist/theme-github-*.css ────────────────────────────────────►│ + ▼ scripts/assembleForgejo.ts + dist/forgejo/templates, dist/forgejo/public/assets/{css,js} + │ + ┌──────────────────────┴──────────────────────┐ + ▼ ▼ + local preview (dev/) release archives (.github/actions) +``` + +## Source layout + +| Path | Holds | Ends up in | Loaded by | +| ------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------ | -------------------------------------- | +| `theme.config.ts`, `themes/` | Theme series and their colors | `dist/theme-github-*.css` | Forgejo's theme setting | +| `primer/` | Primer design tokens, generated by `bun update:primer` | the theme stylesheets | `themes/github.ts` | +| `src/` | The generator: palettes, CSS variables, Vite plugin | nothing; it runs at build time | `vite.config.ts` | +| `styles/` (`*.ts`) | Theme-wide overrides of Forgejo's markup | every theme stylesheet | the theme stylesheet | +| `templates/` | Overrides of Forgejo templates, at Forgejo's paths | `dist/forgejo/templates` | Forgejo, in place of its own template | +| `components///` | Reusable UI: template, stylesheet, script and tests | `templates/components/`, `css/components/`, `js/components/` | `templates/base/component_assets.tmpl` | +| `styles/pages/*.css` | Layout of one page or a family of pages | `css/components/pages/` | `templates/base/component_assets.tmpl` | +| `public/*.css`, `public/assets/js/*` | Stylesheets and scripts that a template links itself | `css/`, `js/` | the template that needs it | +| `dev/` | Local preview: Compose file, preview tool, fixtures | nothing | `bun dev:forgejo*` | + +`css/` and `js/` are under `dist/forgejo/public/assets`. + +## Theme stylesheets + +The theme stylesheets come from a TypeScript pipeline that turns color definitions into CSS variables and appends the +style overrides: + +1. `theme.config.ts` declares the theme series (default, colorblind, tritanopia, high contrast, pink, gitea, catppuccin) + and their variants. A series with both a dark and a light variant also gets an auto theme. +2. `themes/` defines the colors of each variant in one of three input formats. +3. `src/palette` converts each format into `ThemeVars`, a flat map of CSS variable values. +4. `src/core` writes those values as CSS through vanilla-extract, captured by `compileCSS` in `src/vanilla-extract.ts`. +5. The Vite plugin in `src/vite-plugin` emits one file per variant: theme meta info, variables, syntax highlighting and + the overrides from `styles/`, minified by LightningCSS. An auto theme only imports its dark and light files by + `prefers-color-scheme`. + +```text +theme-github-dark.css: + ├── gitea-theme-meta-info { --theme-color-scheme: dark; ... } + ├── :root { --color-primary: #4493f8; ... } (200+ variables) + ├── .chroma { ... }, .code-editor-container { ... } (syntax highlighting) + └── style overrides from styles/ - // 5. Serialize captured CSS objects to strings - return transformCss({ - localClassNames: Array.from(localClassNames), - composedClassLists, - cssObjs: capturedCssObjs, - }).join("\n"); -} +theme-github-auto.css: + ├── @import "./theme-github-dark.css?v=" (prefers-color-scheme: dark); + ├── @import "./theme-github-light.css?v=" (prefers-color-scheme: light); + └── gitea-theme-meta-info { --theme-color-scheme: auto; ... } ``` -**How it works:** +### Color input formats -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 +Each source of colors has its own structure, and all three converge on `ThemeVars`: -**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`: - -``` +```text 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`. +The `theme` path needs only the base colors plus primary and secondary. `theme2ThemeVars()` in +[src/palette/theme.ts](src/palette/theme.ts) derives the rest: lightness and alpha scales, badge and message colors and +ANSI console colors. On dark themes the scales run the other way, so "dark" variants keep their contrast. +`scaleColorLight()` in [src/functions/color.ts](src/functions/color.ts) scales lightness proportionally, as Sass's +`color.scale()` does, so a change stays visible near black and white. -In [src/types/vars.ts](src/types/vars.ts): +### CSS variable names -```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: +`createGlobalThemeContract` with the `varMapper` in [src/types/vars.ts](src/types/vars.ts) maps typed paths to the +variable names Forgejo expects: it joins the path, drops `self` and strips the `num` prefix. | TypeScript access | CSS variable | | ---------------------------------------------- | -------------------------------------- | @@ -243,373 +103,148 @@ This creates a bidirectional mapping: | `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): +### Capturing vanilla-extract output -### Phase 1: Configuration Resolution (`config` hook) +Vanilla-extract's own Vite plugin writes one CSS file per module. The theme needs one file per theme, so `compileCSS` +uses the low-level adapter API instead: it installs an adapter that captures `appendCss` and class name registrations, +runs the caller's `createGlobalTheme` and `globalStyle` calls, removes the adapter and serializes the captured objects +with `transformCss`. Each call is self-contained, so no state leaks from one theme into the next. -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`) +### Style overrides -### Phase 2: CSS Generation (`generateBundle` hook) +The overrides in `styles/` are `css` template literals, composed explicitly with `cssCombine()` so their order is +visible: -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) - └── (~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: - -``` +```text 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 -│ └── version.ts # Version management -├── dist/ # Build output (30+ CSS files) -└── templates/ # Optional Gitea Go template overrides + ├── public/ base elements: buttons, inputs, dropdowns, menus, tables, modals + ├── components/ Forgejo pages styled by Forgejo's own markup: navbar, heatmap, issues, repository, Actions + └── templates/ markup that only the theme's older repository templates render +styles/common/ reusable fragments (cssStyle) for the files above +styles/themes/ styles for one theme series, passed as `styles` in theme.config.ts ``` ---- - -## Key Design Decisions & Trade-offs +The `css` tag in [src/core/css.ts](src/core/css.ts) parses each literal with LightningCSS, so invalid CSS fails the +build with the file and line of the literal. `cssStyle()` turns a typed vanilla-extract style object into a reusable +declaration block, such as `primaryButtonStyle` in `styles/common/button.ts`. Every color is a theme variable. -### Decision 1: Vanilla-Extract for CSS authoring only, not runtime +## Template pack -**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. +### Templates -**Rejected:** Using vanilla-extract's built-in Vite plugin (outputs per-module CSS files, unsuitable for single-file -theme output). +`templates/` mirrors Forgejo's `templates` directory. Forgejo renders a file from its custom templates directory in +place of its own file at the same path, so each override is Forgejo 17.0.0's template with the theme's changes. An +override keeps Forgejo's routes, form fields, permission checks and data, and changes the markup around them. A comment +at the top of a template says why it departs from Forgejo where that is not obvious, as in +`templates/base/head_style.tmpl`. -**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. +### Components -### Decision 2: Single-file CSS output +`components/` holds reusable UI, grouped by what it knows about: `common` for controls that know no routes (dropdown, +search, sidebar, counter), and `dashboard`, `package`, `project` and `repository` for pieces of those pages. A component +directory keeps its template partials, stylesheet, browser script and tests together. Pages call a partial by its path: -**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 +```gotemplate +{{template "components/repository/results/results" dict "Data" . "View" "overview" "Pagination" "org/repository_pagination"}} +``` -**Choice:** Use LightningCSS for build-time CSS syntax validation (in `css()` template tag) and output minification (in -Vite plugin). +The assembly copies each `.tmpl` to `dist/forgejo/templates/components///`, and its `.css` and `.js` to +`public/assets/css/components///` and `public/assets/js/components///`. +`templates/base/component_assets.tmpl`, included at the top of `base/head_navbar.tmpl`, links the component stylesheets +and scripts and the page stylesheets, so they load on every page with the navbar. +[CONTRIBUTING.md](CONTRIBUTING.md#where-new-styles-go) describes the shared controls and where new styles go. -**Rejected:** PostCSS (slower), esbuild CSS (less feature-complete), no validation at all. +### Page stylesheets and page assets -**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. +`styles/pages/*.css` is plain CSS for the layout of one page or a family of pages, scoped by a page prefix such as +`profile-` or `org-`. The assembly copies every file in `styles/pages` to `public/assets/css/components/pages/`; a new +one only needs its `` in `templates/base/component_assets.tmpl`, which a test checks. `public/` holds stylesheets +and scripts that a template links itself, such as the Actions run view's; they load only on the pages that render that +template. -### Decision 4: Bun-only runtime +### Browser scripts -**Choice:** Require Bun ≥ 1.3.14 for development and building. +Scripts are plain JavaScript without a bundler, most loaded with `defer`. Each wraps itself in a function, looks up the +markup it enhances, often by a `data-theme-*` attribute, and returns when the page does not have it. They enhance +server-rendered markup that works without them: `components/repository/results/results.js` replaces the result list with +the same GET route's response, and the form still submits without it. Script tests load the script into a `node:vm` +context with a stubbed document. -**Rejected:** Cross-runtime support (Node + Bun). +### Asset versions -**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). +Forgejo serves custom assets with a long cache lifetime and versions the theme stylesheet link with its own release +only. Templates therefore link assets without a version, and the assembly adds one: every `{{AssetUrlPrefix}}` link gets +the content hash of the file it loads, the theme link in `base/head_style.tmpl` gets the hash of all theme files, and +each auto theme's `@import` gets the hash of the imported file. -### Decision 5: Package self-referencing for internal imports +## Local preview -**Choice:** Use `@lutinglt/gitea-github-theme/core` style imports even within the same package, via `exports` mappings -in `package.json`. +`dev/docker-compose.yml` runs the Forgejo release pinned in `package.json` (`17.0.0-rootless`) and mounts +`dev/.generated/public/assets/css`, `dev/.generated/public/assets/js` and `dev/.generated/templates` read-only into its +custom directory. These are a copy of `dist/forgejo` with the preview's automatic sign-in and page reload added, so the +hooks never reach a release and `dist` stays as `bun bundle` wrote it. The preview tool in `dev/preview` (started by +`dev/dev.ts`) builds, starts Forgejo, seeds it from `dev/fixtures`, and rebuilds and restarts on source changes. +[README.md](README.md#local-forgejo-preview) covers its use and [dev/fixtures/README.md](dev/fixtures/README.md) the +fixture format. -**Rejected:** Relative path imports everywhere. +## Release archives -**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. +`.github/actions/build-release-assets/package.sh` packs the theme stylesheets into three archives (base, accessibility, +extras) and the template pack into `theme-github-templates.tar.gz`: the templates, all scripts and every stylesheet +except the theme stylesheets, laid out to extract into Forgejo's custom directory. ---- +## Key decisions -## Adding a New Theme +### Theme-wide styles in the theme stylesheet, the rest beside the templates -### Quick Path (15 color definitions) +Forgejo links one stylesheet for the selected theme, so the overrides in `styles/` are copied into each of the 23 +non-auto theme files. Component and page stylesheets ship once, beside the templates, but only installs with the +template pack load them. A rule for Forgejo's own markup therefore belongs in `styles/`, where installs with only the +stylesheets get it too, and a rule for markup that only the theme's templates render belongs beside that template. -1. Create `themes/my-theme.ts`: +### Vanilla-extract for authoring only -```typescript -import { defineTheme } from "@lutinglt/gitea-github-theme/core"; -import type { ThemeColor, Syntax } from "@lutinglt/gitea-github-theme/core"; +The theme uses vanilla-extract's `createGlobalThemeContract` and `createGlobalTheme` for typed CSS variables and +serializes them at build time through the adapter API. The adapter API is undocumented and may change between versions; +the locked `@vanilla-extract/css` version and the palette snapshot tests catch changes in the output. -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 */ - }, -}; +### LightningCSS for validation and minification -const syntaxColor: Syntax = { - /* Code highlighting colors */ -}; +LightningCSS checks every `css` literal and minifies the output. It recovers from many syntax errors instead of failing, +so validation catches severe errors only; the build output tests cover part of the rest. -export default defineTheme({ colorType: "theme", themeColor, syntaxColor }); -``` +### Bun only -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 }, - }, -} -``` +Development and builds require Bun 1.3.14 or later, which runs TypeScript natively and gives the stack traces that +`getCallerInfo` parses. -3. Run `bun bundle` — the theme CSS will appear in `dist/`. +### Package self-references -### Full Path (Primer tokens) +Modules import each other through the package's own exports, such as `@lutinglt/gitea-github-theme/core`, with `paths` +in `tsconfig.json` for editors. Import paths stay the same whether the code runs as a dependency or in this repository. -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. +## Adding a theme ---- +Define the colors in `themes/` with `defineTheme`, register the series in `theme.config.ts` and run `bun bundle`; the +stylesheets appear in `dist/`. [CONTRIBUTING.md](CONTRIBUTING.md#color-theme-contributions) has a complete example. For +exact GitHub colors, use the `github` path with Primer tokens, as `themes/github.ts` and `src/palette/github.ts` do. -## Testing Strategy +## Tests -| 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 | +`bunx vitest run` runs every test; `bun bundle` must run first for the build output tests. ---- +| Area | Location | What it verifies | +| ------------ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| Generator | `src/core`, `src/functions`, `src/utils` | `css()` validation and composition, color scaling, variable name extraction, `deepOverride` | +| Palette | `src/palette/*.test.ts` | Snapshots of the color conversion chains and valid color values | +| Build output | `src/build-output.test.ts` | Theme files and their variables, Forgejo-specific rules, every shipped stylesheet unchanged and linked, every asset link versioned | +| Page scripts | `src/navbar-drawer.test.ts`, `src/repo-mirror.test.ts`, `src/actions-run-graph.test.ts` | Scripts in `public/assets/js` | +| Components | `components/**/*.test.ts` | Each component's template contract, styles and script behavior | +| Preview | `dev/preview/**/*.test.ts`, `src/preview-*` | Fixture checks without Docker and the preview's reload hooks | -## Related Documents +## Related documents -- [CONTRIBUTING.md](CONTRIBUTING.md) — Development environment setup and contribution guide -- [README.md](README.md) — User-facing installation and usage -- [primer/README.md](primer/README.md) — Auto-generated Primer token documentation +- [CONTRIBUTING.md](CONTRIBUTING.md): development setup, conventions and pull requests +- [README.md](README.md): installation and the local preview +- [dev/fixtures/README.md](dev/fixtures/README.md): the preview's fixtures +- [primer/README.md](primer/README.md): the generated Primer tokens diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 196ea8a2..18fc9d0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,35 +2,41 @@ ## Directory Structure -| Directory | Description | -| ----------------- | ----------------------------------------------------------------- | -| src | Theme generation framework and utilities | -| src/core | Theme generation framework (CSS vars, syntax highlighting styles) | -| src/functions | Theme utilities (color calculations, etc.) | -| src/palette | Theme palettes (GitHub / Gitea / Catppuccin) | -| src/types | Theme color variable type definitions | -| src/vite-plugin | Theme Vite build plugin | -| styles | Theme style overrides | -| styles/common | Reusable CSS style fragments | -| styles/public | Base element or cross-page element styles | -| styles/components | Page-specific styles | -| styles/templates | Template-specific styles | -| themes | Color theme definitions | -| primer | Auto-generated Primer design tokens | -| scripts | Build and utility scripts | -| templates | Gitea template overrides (optional) | -| gitea | Upstream Gitea reference files | -| screenshots | Theme screenshots | +| Directory | Description | +| -------------------------- | --------------------------------------------------------------------- | +| src | Theme generation framework and utilities | +| src/core | Theme generation framework (CSS vars, syntax highlighting styles) | +| src/functions | Theme utilities (color calculations, etc.) | +| src/palette | Theme palettes (GitHub / Gitea / Catppuccin) | +| src/types | Theme color variable type definitions | +| src/vite-plugin | Theme Vite build plugin | +| styles | Theme-wide overrides in TypeScript, built into every theme stylesheet | +| styles/common | Reusable CSS style fragments | +| styles/public | Base element or cross-page element styles | +| styles/components | Overrides of Forgejo pages by Forgejo's own markup | +| styles/templates | Styles for markup only the theme's older repository templates render | +| styles/pages | Page-only CSS for the theme's templates | +| components | Reusable UI: template partials, stylesheet, browser script and tests | +| templates | Forgejo template overrides (optional) | +| public | Stylesheets and scripts that one page loads from its template | +| themes | Color theme definitions | +| primer | Auto-generated Primer design tokens | +| scripts | Build and utility scripts, including the template pack assembly | +| dev | Local Forgejo preview and its fixtures | +| .github/assets/screenshots | Theme screenshots | + +[ARCHITECTURE.md](ARCHITECTURE.md) explains how these parts are built into the theme stylesheets and the template pack. ## Contribution Notes -Style contributions are not recommended, as Gitea themes are ultimately delivered as a single CSS file, which introduces -issues like ordering/override conflicts and broad-reaching style impacts. +Style contributions are not recommended, as the theme-wide overrides are built into every theme stylesheet and the +shared components load on every page, which introduces issues like ordering/override conflicts and broad-reaching style +impacts. Each developer has their own approach, making it difficult for review to catch these issues, greatly increasing maintenance difficulty. -If you have confirmed understanding of Gitea's style layout and the project's approach, please first file an Issue to +If you have confirmed understanding of Forgejo's markup and the project's approach, please first file an Issue to confirm scope and expected results, then develop and submit a PR. If you believe you have a better approach, feel free to submit an Issue. @@ -113,10 +119,11 @@ Environment variables are used to send compiled themes to a server for quick pre ### Compile Themes -Compile and sync to remote server for preview: +To check changes in a running Forgejo, use the local preview described in [README.md](README.md#local-forgejo-preview). +To compile and sync to a remote server instead: ```bash -bun bundle:dev +bun dev ``` Compile all themes: @@ -137,6 +144,24 @@ For PR submission — check and compile all project code with formatting: bun commit ``` +## Upgrading Forgejo + +The files in `templates` are Forgejo's templates with the theme's changes. The `forgejo` field of `package.json` pins +the Forgejo release they were copied from; the dev preview runs the same release, and a test checks that +`dev/docker-compose.yml` names its image. + +`bun drift` checks out the templates of the pinned release into `node_modules/.cache/forgejo` (git and network on the +first run only) and lists each override by the lines the theme added and removed, the largest first, then the templates +Forgejo does not have. `bun drift 17.1.0` does the same against another release. + +To move to a new release: + +1. Run `bun drift `, for example `bun drift 17.0.0 17.1.0`. It lists the overridden templates Forgejo + changed between the two releases, with Forgejo's diff of each, and the ones it added or removed. +2. Port each change into the override, and rename or remove an override whose file Forgejo moved or removed. +3. Set `forgejo` in `package.json` and the image in `dev/docker-compose.yml` to the new release, then check the changed + pages in the dev preview. + ## Development Conventions `src`, `styles`, and `themes` are the project's main directories. @@ -216,8 +241,8 @@ export default defineTheme({ colorType: "gitea", themeColor: giteaColor, syntaxC ``` After completing theme color development, register the theme series in `theme.config.ts`, compile it, and take -screenshots in a Gitea instance. Place them in the `screenshots` directory with filenames matching the theme names. -(Cloning the repository is recommended to avoid leaking personal information.) +screenshots in a Forgejo instance. Place them in the `.github/assets/screenshots` directory with filenames matching the +theme names. (Cloning the repository is recommended to avoid leaking personal information.) Then add screenshot information to `README.md`. @@ -225,22 +250,60 @@ For color reuse patterns, see the colorblind theme implementation in `themes/git ## Theme Style Contributions -Theme styles use the `css()` template string for development. Strings are processed by LightningCSS and support CSS -nesting syntax. Do not use SCSS functions; for complex processing, use TypeScript libraries such as the bundled -`color2k` library. +The TypeScript styles in `styles/` use the `css()` template string for development. Strings are processed by +LightningCSS and support CSS nesting syntax. Do not use SCSS functions; for complex processing, use TypeScript libraries +such as the bundled `color2k` library. For complex logic, it is recommended to extract it into functions under the `src/functions` directory. +### Where new styles go + +Choose the place by the markup a rule styles and the pages that need it: + +- **Forgejo's own markup, on any page:** a `css` fragment in `styles/`, under `styles/public` for base elements or + `styles/components` for one area of Forgejo. It is built into every theme stylesheet, so installs without the + templates get it too. +- **Reusable UI:** a directory under `components///` with its template partials, stylesheet, script and + tests. Link its stylesheet and script in `templates/base/component_assets.tmpl`; `bun bundle` copies the files. +- **Layout of one page or a family of pages:** plain CSS in `styles/pages/.css`. Add the file to the list in + `scripts/assembleForgejo.ts` and link it in `templates/base/component_assets.tmpl`. +- **An asset that one page template loads:** `public/.css` or `public/assets/js/.js`, linked from that + template. Styles that several pages share belong in a component. + +Link assets as `{{AssetUrlPrefix}}/css/...` or `{{AssetUrlPrefix}}/js/...` without a version; the build adds the content +hash. + +### Name classes by scope + +Name the classes the theme adds by their scope: + +- `theme-*` for shared components, such as `theme-search-field` and `theme-counter`, with `data-theme-*` attributes as + script hooks. +- `github-*` for layout and state hooks on overridden templates, such as `github-issues` or `github-embedded`. +- A page prefix such as `profile-`, `org-` or `actions-` for page-only CSS, such as `profile-avatar-card`. + +### Keep the stacking order + +Keep stacked layers within the scale in `components/common/motion/motion.css`: + +| Layer | `z-index` | +| --------------------------------------- | ----------- | +| Popovers and positioned page content | up to 100 | +| Repository tabs (`.repository-tab-nav`) | 199 | +| Navbar (`#navbar`) | 200 | +| Toasts | 500 | +| Dimmers and modals | 1000 and up | + ### Reuse shared page controls Use `components/common/*/*.css` for sidebars, search fields, selects, counters, switches, empty states, and compact -buttons, and `components/common/dropdown/*` for dropdown variants. `templates/base/head_navbar.tmpl` loads the shared -styles and controllers for all pages. Keep page layout rules in the page stylesheet and pass the page's existing routes, -permissions, and form values into the shared controls. +buttons, and `components/common/dropdown/*` for dropdown variants. `templates/base/component_assets.tmpl`, included from +`templates/base/head_navbar.tmpl`, loads the shared styles and controllers for all pages. Keep page layout rules in the +page stylesheet and pass the page's existing routes, permissions, and form values into the shared controls. - **Sidebar:** add `theme-sidebar-layout` and a unique `data-theme-sidebar` storage key to the page, then use `theme-sidebar`, `theme-sidebar-content`, and `theme-sidebar-main` for its children. Include - `shared/theme/sidebar_toggle` inside the sidebar and a button with `data-theme-sidebar-toggle` and + `components/common/sidebar/toggle` inside the sidebar and a button with `data-theme-sidebar-toggle` and `data-theme-sidebar-expand` beside the page title. The controller preserves desktop collapse preferences and starts collapsed on mobile. See `templates/repo/issue/sidebar.tmpl` and `templates/org/repository_nav.tmpl`. For links to sections of a long page, like GitHub's "On this page", add a `nav` with `data-theme-sidebar-outline` holding a @@ -278,10 +341,10 @@ permissions, and form values into the shared controls. - **Label:** use Forgejo's native `ui basic label` for outline labels such as visibility or package type; do not build a page-specific pill. - **Select:** wrap a native `