Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
727 changes: 181 additions & 546 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

133 changes: 98 additions & 35 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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 <pinned> <new>`, 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.
Expand Down Expand Up @@ -216,31 +241,69 @@ 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`.

For color reuse patterns, see the colorblind theme implementation in `themes/github.ts`.

## 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/<group>/<name>/` 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/<page>.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/<page>.css` or `public/assets/js/<page>.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
Expand Down Expand Up @@ -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 `<select class="theme-select">` in `<span class="theme-select-wrap">`. The options stay in
the page template. See `templates/user/profile_packages.tmpl`.
the page template. See `templates/user/profile_package_versions.tmpl`.

For repository filtering, add `data-theme-repository-search` to the form and render
`components/repository/results/results` after it. `repository-search.js` updates that result fragment through the same
`components/repository/results/results` after it. Its `results.js` updates that result fragment through the same
server-rendered GET route, preserves search and sort values, and cancels obsolete requests. Checkbox criteria combine,
while opposing values of one criterion are exclusive. Clear filters is an action, not a selectable option. Keep
`tab=overview` or `tab=repositories` in the organization form; query parameters alone do not determine the page layout.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,5 @@ to copy the generated files into the running container.
The preview automatically reloads open pages after a successful rebuild or `dev:forgejo:sync`. Each refresh uses
versioned theme stylesheets (including auto-theme imports) and custom scripts, so browser cache clearing is unnecessary.
Run `bun dev:forgejo:watch` while editing to rebuild, restart, and reload automatically. These reload hooks exist only
in `dev/.generated/templates` and the local preview output; release archives contain the normal theme assets.
in the preview's own copy of the build under `dev/.generated`; `dist` and release archives contain the normal theme
assets.
4 changes: 2 additions & 2 deletions dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ services:
condition: service_completed_successfully
volumes:
- forgejo-dev-data:/var/lib/gitea
- ../dist/forgejo/public/assets/css:/var/lib/gitea/custom/public/assets/css:ro
- ../dist/forgejo/public/assets/js:/var/lib/gitea/custom/public/assets/js:ro
- ./.generated/public/assets/css:/var/lib/gitea/custom/public/assets/css:ro
- ./.generated/public/assets/js:/var/lib/gitea/custom/public/assets/js:ro
- ./.generated/templates:/var/lib/gitea/custom/templates:ro

volumes:
Expand Down
14 changes: 14 additions & 0 deletions dev/fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ bun dev:forgejo
Seeding stops with a message naming the fixture when something does not line up, for example a pull request from a
branch that `history.json` never creates. `bun vitest` runs the same checks without Docker.

## Sign in as another user

The preview signs in as `alex` / `preview` by itself. Every other user in `owner.json`, such as `sam`, has the password
`preview-fixture` (`FIXTURES.userPassword` in `dev/preview/config/constants.ts`). To sign in as `sam`, turn the
automatic sign-in off:

```bash
FORGEJO_PREVIEW_AUTO_LOGIN=false bun dev:forgejo:sync
```

Then sign out from the avatar menu and sign in as `sam` / `preview-fixture`. Start the watcher with the same variable to
keep it off while editing. A later sync without it turns the automatic sign-in back on, which only acts while nobody is
signed in, so `sam` stays signed in until signing out.

## Times

Every time is written as `daysAgo`, optionally with a `time` of day in UTC, and is resolved when the preview is seeded.
Expand Down
18 changes: 11 additions & 7 deletions dev/preview/PreviewApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class PreviewApplication {
this.reloader = new PreviewReloader(config);
this.compose = new ForgejoCompose(config, runner);
this.readiness = new ForgejoReadiness(config);
this.synchronizer = new ThemeSynchronizer(this.compose);
this.synchronizer = new ThemeSynchronizer(config, this.compose);
this.admin = new PreviewAdmin(config, this.compose);
this.autoLogin = new AutoLoginTemplate(config);
this.watcher = new SourceWatcher(config);
Expand All @@ -46,8 +46,7 @@ export class PreviewApplication {
this.compose.stop();
return;
case PREVIEW_COMMANDS.sync:
this.autoLogin.prepare();
this.reloader.prepare();
this.prepare();
this.synchronizer.sync();
this.reloader.publish();
return;
Expand All @@ -67,8 +66,7 @@ export class PreviewApplication {

private async start(): Promise<void> {
this.builder.build();
this.autoLogin.prepare();
this.reloader.prepare();
this.prepare();
this.compose.start();
await this.readiness.wait();
this.reloader.publish();
Expand All @@ -79,13 +77,19 @@ export class PreviewApplication {
);
}

/** Copies the build into dev/.generated and adds the preview's sign-in and reload hooks there. */
private prepare(): void {
this.autoLogin.prepare();
this.synchronizer.stage();
this.reloader.prepare();
}

private async watch(): Promise<never> {
await this.start();
console.log("Watching theme sources. Press Ctrl+C to stop.");
return this.watcher.watch(() => {
this.builder.build();
this.autoLogin.prepare();
this.reloader.prepare();
this.prepare();
this.synchronizer.sync();
this.reloader.publish();
console.log("Theme preview updated; open preview pages reload automatically.");
Expand Down
4 changes: 4 additions & 0 deletions dev/preview/config/PreviewConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export class PreviewConfig {
public readonly themeTemplatesDirectory: string;
public readonly generatedTemplatesDirectory: string;
public readonly autoLoginTemplateFile: string;
public readonly themeAssetsDirectory: string;
public readonly generatedAssetsDirectory: string;
public readonly fixturesDirectory: string;
public readonly fixtureDatabaseFile: string;
public readonly sourcePaths: readonly string[];
Expand All @@ -36,6 +38,8 @@ export class PreviewConfig {
this.themeTemplatesDirectory = join(this.projectDirectory, "dist/forgejo/templates");
this.generatedTemplatesDirectory = join(this.projectDirectory, "dev/.generated/templates");
this.autoLoginTemplateFile = join(this.projectDirectory, AUTO_LOGIN.templateDirectory, AUTO_LOGIN.templateFile);
this.themeAssetsDirectory = join(this.projectDirectory, "dist/forgejo/public/assets");
this.generatedAssetsDirectory = join(this.projectDirectory, "dev/.generated/public/assets");
this.fixturesDirectory = join(this.projectDirectory, FIXTURES.directory);
this.fixtureDatabaseFile = join(this.projectDirectory, FIXTURES.localDatabaseFile);
this.sourcePaths = SOURCE_PATHS.map(path => join(this.projectDirectory, path));
Expand Down
13 changes: 13 additions & 0 deletions dev/preview/config/constants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { expect, it } from "vitest";

import pkg from "../../../package.json";
import { COMPOSE } from "./constants.ts";

it("runs the Forgejo release that package.json pins", () => {
const compose = readFileSync(join(import.meta.dirname, "../../docker-compose.yml"), "utf8");
const image = /^\s+image: (code\.forgejo\.org\/forgejo\/forgejo:\S+)$/m.exec(compose)?.[1];
expect(image).toBe(`code.forgejo.org/forgejo/forgejo:${pkg.forgejo}-rootless`);
expect(COMPOSE.image).toBe(image);
});
5 changes: 4 additions & 1 deletion dev/preview/config/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pkg from "../../../package.json";

export const COMMANDS = {
bun: "bun",
docker: "docker",
Expand All @@ -10,7 +12,8 @@ export const COMPOSE = {
file: "dev/docker-compose.yml",
service: "forgejo",
container: "forgejo-github-theme-dev",
image: "code.forgejo.org/forgejo/forgejo:16.0.3-rootless",
/** The pinned Forgejo release of package.json; docker-compose.yml names the same image. */
image: `code.forgejo.org/forgejo/forgejo:${pkg.forgejo}-rootless`,
} as const;

export const PREVIEW = {
Expand Down
5 changes: 3 additions & 2 deletions dev/preview/forgejo/ForgejoCompose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ export class ForgejoCompose {
this.run(["stop", this.config.composeService]);
}

public restart(): void {
this.run(["restart", this.config.composeService]);
/** Unlike a restart, this also applies changed mounts in the Compose file; the data volume stays. */
public recreate(): void {
this.run(["up", "-d", "--force-recreate", "--no-deps", this.config.composeService]);
}

public execute(args: readonly string[]): void {
Expand Down
Loading
Loading