Skip to content

Latest commit

 

History

History
257 lines (194 loc) · 18 KB

ARCHITECTURE.md

File metadata and controls

257 lines (194 loc) · 18 KB

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.1).

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.

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/<group>/<name>/ 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.
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/

theme-github-auto.css:
  ├── @import "./theme-github-dark.css?v=<hash>" (prefers-color-scheme: dark);
  ├── @import "./theme-github-light.css?v=<hash>" (prefers-color-scheme: light);
  └── gitea-theme-meta-info { --theme-color-scheme: auto; ... }

Color input formats

Each source of colors has its own structure, and all three converge on ThemeVars:

Primer tokens ──→ GitHubColor ──→ ThemeColor ──→ ThemeVars
Catppuccin flavor ──────────────→ ThemeColor ──→ ThemeVars
GiteaColor ────────────────────────────────────→ ThemeVars
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

The theme path needs only the base colors plus primary and secondary. theme2ThemeVars() in 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 scales lightness proportionally, as Sass's color.scale() does, so a change stays visible near black and white.

CSS variable names

createGlobalThemeContract with the varMapper in 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
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

Capturing vanilla-extract output

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.

Style overrides

The overrides in styles/ are css template literals, composed explicitly with cssCombine() so their order is visible:

styles/index.ts
  ├── 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

The css tag in 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.

Template pack

Templates

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.1'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.

Components

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:

{{template "components/repository/results/results" dict "Data" . "View" "overview" "Pagination" "org/repository_pagination"}}

The assembly copies each .tmpl to dist/forgejo/templates/components/<group>/<name>/, and its .css and .js to public/assets/css/components/<group>/<name>/ and public/assets/js/components/<group>/<name>/. 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 describes the shared controls and where new styles go.

Page stylesheets and page assets

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 <link> 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.

Browser scripts

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.

Asset versions

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.

Theme version

The footer in base/footer_content.tmpl links the theme's repository with its version. The template declares {{$themeVersion := "dev"}} and the assembly replaces the value: with THEME_VERSION, which the release build sets to the release tag, or else with git describe --tags, such as 1.4.0-3-g54b4d73 for a build three commits after 1.4.0. Admins hide the credit with --custom-footer-credit: none.

Local preview

dev/docker-compose.yml runs the Forgejo release pinned in package.json (17.0.1-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 covers its use and dev/fixtures/README.md the fixture format.

Release archives

.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

Theme-wide styles in the theme stylesheet, the rest beside the templates

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.

Vanilla-extract for authoring only

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.

LightningCSS for validation and minification

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.

Bun only

Development and builds require Bun 1.3.14 or later, which runs TypeScript natively and gives the stack traces that getCallerInfo parses.

Package self-references

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.

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 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.

Tests

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